性能优化-App发动优化

发动状况

运用有三种发动状况,每种状况都会影响运用向用户显现所需的时刻:冷发动、温发动与热发动。在冷发动中,运用从头开端发动。在另外两种状况中,体系需求将后台运转的运用带入前台。建议始终在假定冷发动的基础上进行优化。这样做也能够提升温发动和热发动的性能。

  • 冷发动
    • 冷发动是指运用从头开端发动:体系进程在冷发动后才创立运用进程。产生冷发动的状况包含运用自设备发动后或体系终止运用后首次发动。
  • 热发动:
    • 在热发动中,体系的一切工作便是将 Activity 带到前台。只需运用的一切 Activity 仍驻留在内存中,运用就不必重复履行方针初始化、布局加载和制作。
  • 温发动
    • 温发动包含了在冷发动期间产生的部分操作;同时,它的开支要比热发动高。有许多潜在状况可视为温发动。例如:
      • 用户在退出运用后又从头发动运用。进程或许未被毁掉,持续运转,但运用需求履行onCreate() 从头开端从头创立 Activity。
      • 体系将运用从内存中开释,然后用户又从头发动它。进程和 Activity 需求重启,但传递到onCreate() 的已保存的实例 state bundle 关于完结此使命有一定助益。

冷发动耗时计算

体系日志计算

在 Android 4.4(API 等级 19)及更高版别中,logcat 包含一个输出行,其间包含名为 Displayed 的值。此值代表从发动进程到在屏幕上完结对应 Activity 的制作所用的时刻。

ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms

假如咱们运用异步懒加载的办法来提升程序画面的显现速度,这一般会导致的一个问题是,程序画面现已显现,同时 Displayed 日志现已打印,可是内容却还在加载中。为了衡量这些异步加载资源所耗费的时刻,咱们能够在异步加载完毕之后调用 activity.reportFullyDrawn() 办法来让体系打印到调用此办法停止的发动耗时。

adb 指令计算

查看发动时刻的另一种办法是运用指令:

adb [-d|-e|-s <serialNumber>] shell am start -S -W com.example.app/.MainActivity -c android.intent.category.LAUNCHER -a android.intent.action.MAIN

发动完结后,将输出:

ThisTime: 415
TotalTime: 415
WaitTime: 437
  • WaitTime:总的耗时,包含前一个运用Activity pause的时刻和新运用发动的时刻;
  • ThisTime表明一连串发动Activity的最终一个Activity的发动耗时;
  • TotalTime表明新运用发动的耗时,包含新进程的发动和Activity的发动,但不包含前一个运用Activity pause的耗时。

开发者一般只需关怀TotalTime即可,这个时刻才是自己运用真实发动的耗时。

CPU Profifile

要在运用发动过程中主动开端记载 CPU 活动,请履行以下操作:

  1. 依次挑选 Run > Edit Confifigurations

    性能优化-App启动优化

  2. Profifiling 标签中,勾选 Start recording CPU activity on startup 周围的复选框。

性能优化-App启动优化

  1. 从菜单中挑选 CPU 记载装备。

    • Sample Java Methods

      对 Java 办法采样:在运用的 Java 代码履行期间,频繁捕获运用的调用仓库。分析器会比较捕获的数据集,

      以推导与运用的 Java 代码履行有关的时刻和资源运用信息。假如运用在捕获调用仓库后进入一个办法并鄙人

      次捕获前退出该办法,分析器将不会记载该办法调用。假如您想要盯梢生命周期如此短的办法,应运用检测

      盯梢。

    • Trace Java Methods

      盯梢 Java 办法:在运转时检测运用,以在每个办法调用开端和结束时记载一个时刻戳。体系会收集并比较这

      些时刻戳,以生成办法盯梢数据,包含时刻信息和 CPU 运用率。

    • Sample C/C++ Functions

      对 C/C++ 函数采样:捕获运用的原生线程的采样盯梢数据。要运用此装备,您有必要将运用布置到搭载

      Android 8.0(API 等级 26)或更高版别的设备上。

    • Trace System Calls

      盯梢体系调用:捕获十分详尽的细节,以便您查看运用与体系资源的交互状况。您能够查看线程状况的确切

      时刻和持续时刻、直观地查看一切内核的 CPU 瓶颈在何处,并增加要分析的自定义盯梢事情。要运用此配

      置,您有必要将运用布置到搭载 Android 7.0(API 等级 24)或更高版别的设备上。

      此盯梢装备在 systrace 的基础上构建而成。您能够运用 systrace 指令行实用程序指定除 CPU Profifiler 供给的

      选项之外的其他选项。systrace 供给的其他体系级数据可协助您查看原生体系进程并排查丢帧或帧推迟问

      题。

    1. 点击 Apply

    2. 依次挑选 Run > Profifile,将您的运用布置到搭载 Android 8.0(API 等级 26)或更高版别的设备上。

      性能优化-App启动优化

    点击Stop,结束盯梢后显现:

    性能优化-App启动优化

Call Chart

以图形来呈现办法盯梢数据或函数盯梢数据,其间调用的时刻段和时刻在横轴上表明,而其被调用方则在纵轴上显现。对体系 API 的调用显现为橙色,对运用自有办法的调用显现为绿色,对第三方 API(包含 Java 言语 API)的调用显现为蓝色。 (实际色彩显现有Bug)

微信图片_20220609233124

Call Chart 现已比原数据可读性高很多,但它依然不方便发现那些运转时刻很长的代码,这时咱们便需求运用Flame Chart。

Flame Chart

供给一个倒置的调用图表,用来汇总完全相同的调用仓库。也便是说,将具有相同调用方次序的完全相同的办法或函数收集起来,并在火焰图中将它们表明为一个较长的横条 。

横轴显现的是百分比数值。因为忽略了时刻线信息,Flame Chart 能够展示每次调用耗费时刻占用整个记载时长的百分比。 同时纵轴也被对调了,在顶部展示的是被调用者,底部展示的是调用者。此刻的图表看起来越往上越窄,就好像火焰相同,因而得名: 火焰图。

说白了便是将Call Chart上下调用栈倒过来。

性能优化-App启动优化

Top Down Tree

假如咱们需求更准确的时刻信息,就需求运用 Top Down Tree。 Top Down Tree显现一个调用列表,在该列表中打开办法或函数节点会显现它调用了的办法节点。

关于每个节点,三个时刻信息:

  • Self Time —— 运转自己的代码所耗费的时刻;
  • Children Time —— 调用其他办法的时刻;
  • Total Time —— 前面两者时刻之和。

此视图能够十分方便看到耗时最长的办法调用栈。

Bottom Up Tree

方便地找到某个办法的调用栈。在该列表中打开办法或函数节点会显现哪个办法调用了自己。

Debug API

除了直接运用 Profifile 发动之外,咱们还能够借助Debug API生成trace文件。

public class MyApplication extends Application {
    public MyApplication() { 
        Debug.startMethodTracing("test");
    }
    //
    ..... 
}
public class MainActivity extends AppCompatActivity { 
    @Override public void onWindowFocusChanged(boolean hasFocus) { 
        super.onWindowFocusChanged(hasFocus); 
        Debug.stopMethodTracing();
    }
    //
    .......
}

运转App,则会在sdcard中生成一个enjoy.trace文件(需求sdcard读写权限)。将手机中的trace文件保存至电脑,随后拖入Android Studio即可。

总结

通过东西能够定位到耗时代码,然后查看是否能够进行优化。关于APP发动来说,发动耗时包含Android体系发动APP进程加上APP发动界面的耗时时长,咱们可做的优化是APP发动界面的耗时,也便是说从Application的构建到主界面的 onWindowFocusChanged 的这一段时刻。

StrictMode严苛模式

StrictMode是一个开发人员东西,它能够检测出咱们或许无意中做的事情,并将它们提请咱们留意,以便咱们能够

修复它们。

StrictMode最常用于捕获运用程序主线程上的意外磁盘或网络访问。协助咱们让磁盘和网络操作远离主线程,能够

使运用程序更加滑润、呼应更快。

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        if (BuildConfig.DEBUG) {
            //线程检测战略
            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                    .detectDiskReads() //读、写操作
                    .detectDiskWrites()
                    .detectNetwork() // or .detectAll() for all detectable problems
                    .penaltyLog().build());
            StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                    .detectLeakedSqlLiteObjects() //Sqlite方针泄露
                    .detectLeakedClosableObjects() //未关闭的Closable方针泄露
                    .penaltyLog() //违规打印日志
                    .penaltyDeath() //违规崩溃 
                    .build());
        }
    }
}

发动黑白屏

当体系加载并发动 App 时,需求耗费相应的时刻,这样会造成用户会感觉到当点击 App 图标时会有 “推迟” 现象,为了处理这一问题,Google 的做法是在 App 创立的过程中,先展示一个空白页面,让用户体会到点击图标之后立马就有呼应。

假如你的application或activity发动的过程太慢,导致体系的BackgroundWindow没有及时被替换,就会呈现发动时白屏或黑屏的状况(取决于Theme主题是Dark仍是Light)。消除发动时的黑/白屏问题,大部分App都采用自己在Theme中设置背景图的办法来处理。

<style name="AppTheme.Launcher">
    <item name="android:windowBackground">@drawable/bg</item> 
</style> 
<activity 
          android:name=".activity.SplashActivity"
          android:screenOrientation="portrait" 
          android:theme="@style/AppTheme.Launcher"> 
    <intent-filter> 
        <action android:name="android.intent.action.MAIN" /> 
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter> 
</activity>

然后在Activity的onCreate办法,把Activity设置回本来的主题。

@Override protected void onCreate(Bundle savedInstanceState) {
    //替换为本来的主题在onCreate之前调用
    setTheme(R.style.AppTheme); 
    super.onCreate(savedInstanceState); 
}

这么做,只是提高发动的用户体验。并不能做到真实的加速发动速度

总结

总体

  1. 合理的运用异步初始化、推迟初始化、懒加载机制。
  2. 发动过程防止耗时操作,如数据库 I/O操作不要放在主线程履行。
  3. 类加载优化:提早异步履行类加载。
  4. 合理运用IdleHandler进行推迟初始化。
  5. 简化布局

发动流程

  1. 点击桌面App图标,Launcher进程采用Binder IPC向system_server进程建议startActivity恳求;
  2. system_server进程接收到恳求后,向zygote进程发送创立进程的恳求;
  3. Zygote进程fork出新的子进程,即App进程;
  4. App进程,通过Binder IPC向sytem_server进程建议attachApplication恳求;
  5. system_server进程在收到恳求后,进行一系列准备工作后,再通过binder IPC向App进程发送scheduleLaunchActivity恳求;
  6. App进程的binder线程(ApplicationThread)在收到恳求后,通过handler向主线程发送LAUNCH_ACTIVITY消息;
  7. 主线程在收到Message后,通过反射机制创立方针Activity,并回调Activity.onCreate()等办法。
  8. 到此,App便正式发动,开端进入Activity生命周期,履行完onCreate/onStart/onResume办法,UI烘托结束后便能够看到App的主界面。
  9. Application的构建到主界面的 onWindowFocusChanged 的这一段时刻能够去优化

性能优化-App启动优化

发动加载常见优化战略

一个运用越大,涉及模块越多,包含的服务甚至进程就会越多,如网络模块的初始化,底层数据初始化等,这些加载都需求提早准备好,有些不必要的就不要放到运用中。一般能够从以下四个维度整理发动的各个点:

1、必要且耗时:发动初始化,考虑用线程来初始化

2、必要不耗时:不必处理

3、非必要耗时,数据上报、插件初始化,按需处理

4、非必要不耗时:直接去掉,有需求的时分再加载

将运用发动时要履行的内容按上述分类,按需完成加载逻辑。那么常见的优化加载战略有哪些呢?

异步加载:耗时多的加载放到子线程中异步履行

推迟加载: 非有必要的数据推迟加载

提早加载:运用ContentProvider提早进行初始化

异步加载

异步加载,简单来说,便是运用子线程异步加载。在实际场景中,发动时常常需求对各种第三方库做初始化操作。通过将初始化放到子线程中进行,能够大大加速发动。但是一般,有些业务逻辑是要再第三方库的初始化后才干正常运转的,这时分假如只是简单的放到子线程中跑,不做约束就很或许呈现在没初始化完结就跑业务逻辑,导致异常。这种较为复杂的状况下,能够采用CountDownLatch处理,或者是运用发动器的思维处理。

CountDownLatch运用

class MyApplication extends Application {
    // 线程等待锁
    private CountDownLatch mCountDownLatch = new CountDownLatch(1);
    // CPU核数
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    // 中心线程数
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
    void onCreate() {
        ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);
        service.submit(new Runnable() {
            @Override public void run() {
                //初始化weex,因为Activity加载布局要用到需求提早初始化完结
                initWeex();
                mCountDownLatch.countDown();
            }
        });
        service.submit(new Runnable() {
            @Override public void run() {
                //初始化Bugly,无需关怀是否在界面制作前初始化完
                initBugly();
            }
        });
        //提交其他库初始化,此处省略。。。
        try {
            //等待weex初始化完才走完onCreate
            mCountDownLatch.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运用CountDownLatch在初始化的逻辑不复杂的状况下引荐运用。但假如初始化的几个库之间又有相互依赖,逻辑复杂的状况下,则引荐运用加载器的办法。

发动器的中心如下:

  • 充分运用CPU多核能力,主动整理并次序履行使命;
  • 代码Task化,将发动使命笼统成各个task;
  • 依据一切使命依赖联系排序生成一个有向无环图;
  • 多线程依照线程优先级次序履行

具体完成可参阅:github.com/NoEndToLF/A…

推迟加载

有些第三方库的初始化其实优先级并不高,能够按需加载。或者是运用IdleHandler在主线程闲暇的时分进行分批初始化。按需加载可依据具体状况完成,这儿不做赘述。这儿介绍下运用IdleHandler的运用

 private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {
            //当return true时,会移除掉该IdleHandler,不再回调,当为false,则下次主线程闲暇时会再次回调
            return false;
        }
    };

运用IdleHandler做分批初始化,为什么要分批?当主线程闲暇时,履行IdleHandler,但假如IdleHandler内容太多,则仍是会导致卡顿。因而最好是将初始化操作分批在主线程闲暇时进行

public class DelayInitDispatcher {
    private Queue<Task> mDelayTasks = new LinkedList<>();
    private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {
            //每次履行一个Task,完成分批进行
            if(mDelayTasks.size()>0){
                Task task = mDelayTasks.poll();
                new DispatchRunnable(task).run();
            }
            //当为空时,回来false,移除IdleHandler
            return !mDelayTasks.isEmpty();
        }
    };
    //增加初始化使命
    public DelayInitDispatcher addTask(Task task){
        mDelayTasks.add(task);
        return this;
    }
    //给主线程增加IdleHandler
    public void start(){
        Looper.myQueue().addIdleHandler(mIdleHandler);
    }
}

提早加载

上述计划中初始化最快的时机都是在Application的onCreate中进行,但还有更早的办法。ContentProvider的onCreate是在Application的attachBaseContext和onCreate办法中间进行的。也便是说它比Application的onCreate办法更早履行。所以能够运用这点来对第三方库的初始化进行提早加载。

androidx-startup运用

怎么运用:
第一步,写一个类完成Initializer,泛型为回来的实例,假如不需求的话,就写Unit
class TimberInitializer : Initializer<Unit> {
    //这儿写初始化履行的内容,并回来初始化实例
    override fun create(context: Context) {
        if (BuildConfig.DEBUG) {
            Timber.plant(Timber.DebugTree())
            Timber.d("TimberInitializer is initialized.")
        }
    }
    //这儿写初始化的东西依赖的另外的初始化器,没有的时分回来空List
    override fun dependencies(): List<Class<out Initializer<*>>> {
        return emptyList()
    }
}
第二步,在AndroidManifest中声明provider,并装备meta-data写初始化的类
<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="com.test.pokedex.androidx-startup"
    android:exported=“false"
    //这儿写merge是因为其他模块或许也有相同的provider声明,做兼并操作
    tools:node="merge">
    //当有相互依赖的状况下,写顶层的初始化器就能够,其依赖的会主动搜索到
    <meta-data
        android:name="com.test.pokedex.initializer.TimberInitializer"
        android:value="androidx.startup" />
</provider>

MutilDex 优化

问题:dex 的指令格式规划并不完善,单个 dex 文件中引用的 Java 办法总数不能超过 65536 个,在办法数超过 65536 的状况下,将拆分红多个 dex。一般状况下 Dalvik 虚拟机只能履行通过优化后的 odex 文件,在 4.x 设备上为了提升运用装置速度,其在装置阶段仅会对运用的首个 dex 进行优化。关于非首个 dex 其会在首次运转调用MultiDex.install 时进行优化,而这个优化是十分耗时的,这就造成了 4.x 设备上首次发动慢的问题。

处理办法:

损坏“Dalvik 虚拟机需求加载 odex”这一约束,即绕过 Dalvik 的约束直接加载未经优化的 dex。这个计划的中心在 Dalvik_dalvik_system_DexFile_openDexFile_bytearray 这个 native 函数,它支持加载未经优化后的 dex 文件。具体的优化计划如下:

  1. 首先从 APK 中解压获取原始的非首个 dex 文件的字节码;
  2. 调用 Dalvik_dalvik_system_DexFile_openDexFile_bytearray,逐一传入之前从 APK 获取的 DEX 字节码,完结 DEX 加载,得到合法的 DexFile 方针;
  3. 将 DexFile 都增加到 APP 的 PathClassLoader 的 DexPathList 里;
  4. 拖延异步对非首个 dex 进行 odex 优化。

引入库BoostMultiDex