【项目优化】App启动优化实战

作者: pphdsny | 来源:发表于2018-08-28 17:27 被阅读9次

    前言

    某次在开发阶段,发现App启动过程中既然有3-4s的白屏时间,瞬间慌了,到底干了些什么???

    分析

    启动时间统计

    # 完整命令
    adb [-d|-e|-s <serialNumber>] shell am start -S -W
    com.example.app/.MainActivity
    -c android.intent.category.LAUNCHER
    -a android.intent.action.MAIN
    # 常用命令
    adb shell am start -W packageName/Activity类全路径(启动Activity)
    # -c和-a参数是可选的,允许您为intent指定<category>和<action>。
    

    启动时间如下(小米5s)-Debug环境:

    ThisTime TotalTime WaitTime
    4357 4357 4384

    看到数据还是还是很惊讶的,感觉下载了个线上版本,用上述命令统计了下启动时间 -Release环境:

    ThisTime TotalTime WaitTime
    1279 1279 1304

    心中终于松了一口气,既然是这个版本出的问题,那就开始查原因。

    ThisTime:最后一个启动的Activity的启动耗时;

    TotalTime:自己的所有Activity的启动耗时(多个Activity,如果只启动一个Activity的时候TotalTime=ThisTime);

    WaitTime: ActivityManagerService启动App的Activity时的总时间

    如果只关心某个应用自身启动耗时,参考TotalTime;如果关心系统启动应用耗时,参考WaitTime;如果关心应用有界面Activity启动耗时,参考ThisTime

    详情计算说明可参考:https://www.zhihu.com/question/35487841

    官方指导

    官方指导:launch-time

    官方译文:应用启动时长

    从指导文章中可以看出,App启动时长过长,基本就是做了很多耗时的操作,比如在Application的onCreate中做了很多初始化导致。

    找问题瓶颈

    我们需要知道到底哪些操作引起了耗时,我们需要去分析Application中都做了哪些耗时操作,于是找了以下一些工具:

    • Method Tracing
    • Android Profiler
    • Systrace(待研究)
    • BlockCanary
    • 自己手动统计

    经过一些尝试后发现,只有Systrace和自己手动统计能满足自己需求,后面细说。

    自己手动统计

    没有好的方法只能用笨方法,虽然要改造原有代码,但好在灵活性较高,快速出结果:

        public static void traceMethod(String source, Action0 func) {
            if (PropertyUtils.isProduct()) {
                func.call();
            } else {
                long startTime = System.currentTimeMillis();
                func.call();
                long endTime = System.currentTimeMillis();
                LogUtils.w("Method Measure: source:[" + source + "][" + (endTime - startTime) + "]");
            }
        }
    

    将你需要统计时长的方法,经过traceMethod进行一层包装,在log中进行参看。这个方式是否笨拙,但是有效简单。后续如有其他方式可以再替换。

    时长数据

    [getComponent().inject]--[278]
    [RouterManager.init]--[0]
    [EnvironmentController.getInstance().init(provider)]--[1]
    [UIManager.init(this)]--[0]
    [UtilsManager.init(this)]--[7]
    [DRImageLoader.init(this)]--[242]
    [initLibs]--[252]
    [initViewBinding]--[61]
    [initRouter]--[923] —>37
    [TinkerHelper.initTinker()]--[2]
    [initAppComponent()]--[0]
    [initARouter()]--[1]
    [initCrashHandler()]--[2]
    [initLibs()]--[654]
    [SDKInitializer.initialize(this)]--[60]
    [initPushSdk()]--[47]
    [initStatisticsTools()]--[256]
    [initOnAppProcess()]--[11]

    [Application onCreate]—[2405]

    时长分析

    超过100的都应该优化

    initRouter—923ms->37ms

    if (PropertyUtils.isDebugOpen()) {           // 这两行必须写在init之前,否则这些配置在init过程中将无效
        ARouter.openLog();     // 打印日志
        ARouter.openDebug();   // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
    }
    ARouter.init(this);
    

    罪魁祸首就是下面两行代码,也不知道干了啥,去掉后时间降到37ms

     ARouter.openLog();     // 打印日志
     ARouter.openDebug();
    

    initLibs()— 654ms

    这个方法里面做了很多依赖库的初始化,再进行细分看下各个依赖库的init耗时

    [DRCustomerNetController.getInstance().init()]--[78]
    [initUBT()]--[215]
    [initCustomerId()]--[0]
    [initResourceConfig()]--[3]
    [RongYunManager.init()]--[281]
    [PlatformManager.init()]--[101]
    [getRouterService.init()]--[1]

    需要优化的几个地方:

    • initUBT
    • RongYunManager.init
    • PlatformManager.init

    initUBT—215ms

    牵扯到UBT组件,后续再优化

    RongYunManager.init—281ms

    移至子线程

    PlatformManager.init—101ms

    移至子线程

    getComponent().inject—278ms

    牵扯到组件实例生成,暂不处理

    DRImageLoader.init--242ms

    不可移至子线程,初始化移至第一次调用

    必须在Application中进行调用,底层fresco的imageView在实例化的时候就会去检测

    initStatisticsTools--256ms

    移至子线程

    优化后结果

    ThisTime TotalTime WaitTime
    2838 2838 2850

    [getComponent().inject]--[230]
    [RouterManager.init]--[0]
    [EnvironmentController.getInstance().init(provider)]--[0]
    [UIManager.init(this)]--[1]
    [UtilsManager.init(this)]--[6]
    [DRImageLoader.init(this)]--[78]
    [initLibs]--[89]
    [initViewBinding]--[63]
    [initRouter]--[33]
    [TinkerHelper.initTinker()]--[1]
    [initAppComponent()]--[0]
    [initCrashHandler()]--[1]
    [ISPMUBTProtocol.init()]--[252]
    [initUBT()]--[260]
    [initCustomerId()]--[0]
    [initResourceConfig()]--[6]
    [initAppEnv()]--[269]
    [getRouterService.init()]--[2]
    [initLibs()]--[273]
    [SDKInitializer.initialize(this)]--[44]
    [initPushSdk()]--[42]
    [initOnAppProcess()]--[12]
    Method Measure:Application onCreate耗时:805
    Method Measure:initOnWorkThread耗时:653

    优化后发现Application的onCreate还耗时800多毫秒,主要是由于UBT、Component、Image的初始化导致,暂无较好优化方案,后续观察;

    还有个很奇怪的现象就是App启动要2838,减去Application的805,还剩2000+的时间,这部分时间是谁消耗的,暂无定论,猜测是一些static和单例导致的。由于没有办法去监控,先优化到这样子。

    优化方案

    • 异步初始化(比如一些非必须的组件可以放在子线程进行初始化)
    • 延迟初始化(一般的launchActivity会有几秒的启动屏,可以在里面做一些初始化操作,比如ImageLoader的init)
    • 懒加载,用到才初始化(有很多组件并不是应用启动时候就要使用,这个可以参考Dagger)

    实现代码如下:

    
        /**
         * 在App线程进行初始化
         */
        private void initOnAppProcess() {
            TinkerHelper.initTinker();
            initRouter();
            DRCustomerNetController.getInstance().init();
            initAppComponent();
            initCrashHandler();
            initComponentService();
            //资源下发配置
            initResourceConfig();
            SDKInitializer.initialize(this);
            DRPushManager.initPushSdk();
            initGrowingio();
            registerActivityLifecycleCallbacks(new PageLifecycleCallbacks());
        }
    
        /**
         * 非阻塞的,在子线程进行初始化
         */
        private void initOnWorkThread() {
            new Thread() {
                @Override
                public void run() {
                    long startTime = System.currentTimeMillis();
                  //不与主线程争抢资源
                                android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
                    //self lib
                    initPlatform();
                    //third lib
                    RongYunManager.getInStance().init(innerInstance, AppConstans.RONG_CLOUD_APP_KEY);
                    initBugly();
                    long endTime = System.currentTimeMillis();
                    LogUtils.d("Method Measure:initOnWorkThread耗时:" + (endTime - startTime));
                }
            }.start();
        }
    
    

    后记

    发现小米设备的启动要2s+之后,又用其他设备尝试了下基本在1s以内,通过systrace捕获发现小米中bindApplication就用了1.5s左右。后续有编译了个release版本重新运行了结果如下:

    ThisTime TotalTime WaitTime
    835 835 849

    看到这个结果还是很欣慰的(提速了35%),看来release版本还是做了一些优化,小米系统坑啊!

    相关文章

      网友评论

      • f17cd6e43b75:请教下,是如何保证主线程要使用的时候,子线程的模块已经初始化好了呢?
        pphdsny:@林斌91 没有绝对的方案,而且主线程不可能一直占用CPU的,如果一定要保证的话,billy05提到的方案可以参考,用到时候做个初始化检验
        billy05:可以使用预加载方案:先子线程加载,主线程需要使用的时候如果已加载完成则直接使用,未加载完成则等加载完成后再自动使用。
        这个预加载框架可以很方便地做到这一点:https://github.com/luckybilly/PreLoader
      • 毛毛虫撤回一条消息:~~~楼主~~你提出的异步初始化 有没有限制
        billy05:@pphdsny 即使是启动必须的,如果需要使用的时机比较靠后(比如进入启动页或者主页时才需要),也可以用预加载方案来异步加载,只要保障使用的时候异步加载成功了就行。
        这个预加载框架可以很方便地做到这一点:https://github.com/luckybilly/PreLoader
        pphdsny:没有特别限制,一般不牵扯UI操作,非启动必须项都可以放在异步。
        这个自己得去衡量,因为异步就会牵扯到初始化执行时机的非确定性。

      本文标题:【项目优化】App启动优化实战

      本文链接:https://www.haomeiwen.com/subject/rqcfwftx.html