Android 启动优化

随着项目版本的迭代,App的性能问题会逐渐暴露出来,启动慢,启动白屏黑屏,包越来越大,App耗电量高,而好的用户体验与性能表现紧密相关。今天就来分析关于Android启动优化要怎么做?

为什么出现白屏或者黑屏

冷启动白屏持续时间可能会很长,这是个很不好的体验,一般由以下原因造成:
1、Application的onCreate流程,对于大型的APP来说,通常会在这里做大量的通用组件的初始化操作;
建议:很多第三方SDK都放在Application初始化,我们可以放到用到的地方才进行初始化操作。
2、Activity的onCreate流程,特别是UI的布局与渲染操作,如果布局过于复杂很可能导致严重的启动性能问题;
优化APP启动速度意义重大,启动时间过长,可能会使用户直接卸载APP。

白屏或黑屏,具体是哪一个,取决于app的Theme使用的是dark还是light主题

一、启动的三种方式

App启动方式 冷启动 热启动 温启动

冷启动(Cold start)

冷启动是指APP在手机启动后第一次运行,或者APP进程被kill掉后在再次启动。
可见冷启动的必要条件是该APP进程不存在,这就意味着系统需要创建进程,APP需要初始化。在这三种启动方式中,冷启动耗时最长,对于冷启动的优化也是最具挑战的。因此本文重点谈论的是对冷启动相关的优化。

温启动(Warm start)

App进程存在,当时Activity可能因为内存不足被回收。这时候启动App不需要重新创建进程,但是Activity的onCrate还是需要重新执行的。场景类似打开淘宝逛了一圈然后切到微信去聊天去了,过了半小时再次回到淘宝。这时候淘宝的进程存在,但是Activity可能被回收,这时候只需要重新加载Activity即可。

热启动(Hot start)

App进程存在,并且Activity对象仍然存在内存中没有被回收。可以重复避免对象初始化,布局解析绘制。
场景就类似你打开微信聊了一会天这时候出去看了下日历 在打开微信 微信这时候启动就属于热启动。

二、启动优化时间统计

3.1adb命令 : adb shell am start -S -W 包名/启动类的全限定名 , -S 表示重启当前应用

E:\code\kfz-android>adb shell am start -S -W com.xxx.app/com.xxx.app.xxx.SplashActivity
Stopping: com.xxx.app
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.xxx.app/.business.SplashActivity }
Status: ok
Activity: com.xxx.app/.xxx.SplashActivity
ThisTime: 819
TotalTime: 819
WaitTime: 834
Complete

ThisTime : 最后一个 Activity 的启动耗时(例如从 LaunchActivity - >MainActivity「adb命令输入的Activity」 , 只统计 MainActivity 的启动耗时)
TotalTime : 启动一连串的 Activity 总耗时.(有几个Activity 就统计几个)
WaitTime : 应用进程的创建过程 + TotalTime .

3.2可以通过在代码中增加log来计算启动时间

long startTime = System.currentTimeMillis();    //获取开始时间
doSomething();    //测试的代码段
long endTime = System.currentTimeMillis();    //获取结束时间
System.out.println("程序运行时间:" + (endTime - startTime) + "ms");    //输出程序运行时

3.3 使用systrace

可能在源代码经常看到这种Debug.startMethodTracing(file.getAbsolutePath());统计的代码,是官方提供,分析trace文件

  //把分析结果存在一个文件
File file = new File(Environment.getExternalStorageDirectory(), "kongfz.trace");
Log.e(TAG, "onCreate: " + file.getAbsolutePath());
Debug.startMethodTracing(file.getAbsolutePath());
需要统计的方法
Debug.stopMethodTracing();
获取到的trace文件
获取到的trace文件

可以看到具体哪个方法特别耗时。

3.4 使用profiler来查看方法耗时

profiler面板截图

关于3、4方案查看trace文件可以通过Hdmss或者直接通过AndroidStudio分析。

3.5 使用依赖库 例如AOP 无侵入埋点插桩方式进行时间统计,或者APT注解编译统计,这个暂不举例,可以自行百度使用方法。

3.6根据系统日志来统计启动耗时,在Android Studio中查找已用时间,必须在logcat视图中禁用过滤器(No Filters)。因为这个是系统的日志输出,而不是应用程序的。

比如我们可以通过过滤displayed输出的启动日志. 示例如下:


系统过滤

三、原因分析

1.高耗时任务
数据库初始化、某些第三方框架初始化、大文件读取、MultiDex加载等,导致CPU阻塞
2.复杂的View层级
使用的嵌套Layout过多,层级加深,导致View在渲染过程中,递归加深,占用CPU资源,影响Measure、Layout等方法的速度
3.类过于复杂
Java对象的创建也是需要一定时间的,如果一个类中结构特别复杂,new一个对象将消耗较高的资源,特别是一些单例的初始化,需要特别注意其中的结构
4.主题及Activity配置
有一些App是带有Splash页的,有的则直接进入主界面,由于主题切换,可能会导致白屏,或者点了Icon,过一会儿才出现主界面

四、优化方案

冷启动优化分为可控阶段和不可控阶段

不可控阶段

点击app以后到初始化Application之间这段时间,系统接管,从Zygote进程中fork创建新进程,GC回收等等一系列操作,和我们app无关

可控阶段

作为普通应用,App进程的创建等环节我们是无法主动控制的,可以优化的也就是Application、Activity创建以及回调等过程。
初始化Application开始,如下图:


启动流程

从上图可以看到,整个冷启动流程中至少有两处onCreate,分别是Application和Activity,整个流程都是可控的。所以,onCreate方法做的事情越多,冷启动消耗的时间越长。

关于启动加速方案,Google给出的建议是:

1.利用提前展示出来的Window,快速展示出来一个界面,给用户快速反馈的体验;
2.避免在启动时做密集沉重的初始化(Heavy app initialization);
3.定位问题:避免I/O操作、反序列化、网络操作、布局嵌套等。

官方文档给出的建议中,方向1属于治标不治本,只是表面上快;方向2、3可以真实的加快启动速度。接下来我们就在项目中实际应用一下。

4.1透明主题优化

为了解决启动窗口白屏问题,许多开发者使用透明主题来解决这个问题,但是治标不治本。
会出现点击图标几秒钟没有反应情况,虽然解决了上面这个问题,但是仍然有些不足。


    

4.2设置闪屏图片主题

为了更顺滑无缝衔接我们的闪屏页,可以在启动 Activity 的 Theme中设置闪屏页图片,这样启动窗口的图片就会是闪屏页图片,而不是白屏。
其实只是良好体验,没有做到真正的优化。大部分App都采用这种方式。

    

4.3 优化Application

从用户点击launcher图标到看到界面第一帧为应用启动过程,主要会经过以下这些方法:

main()->Application:attachBaseContext()->onCreate()->Activity:onCreate()->onStart()->onResume()

main->Activity创建的这个过程会经过一系列framework层的操作,对于系统自动执行的操作我们不易进行优化,但是,如果我们继承Application自定义了自己的Application,可以做如下优化:

1.尽量不将一些业务逻辑放于Application中;
2.不以静态变量的方式在Application中保存应用数据;
3.不要把文件、数据库的操作放在Application
我们知道有很多第三方组件(包括App应用本身)都在 Application 中完成初始化操作。但是在 Application 中完成繁重的初始化操作和复杂的逻辑就会影响到应用的启动性能。

分析后发现影响冷启动时间的常见问题如下:

复杂繁琐的布局初始化
阻塞主线程 UI 绘制的操作,如 I/O 读写或者是网络访问.
其它占用主线程的操作

我们可以根据这些组件的轻重缓急之分,对初始化做一下分类 :

必要的组件一定要在主线程中立即初始化(入口 Activity 可能立即会用到)
组件一定要在主线程中初始化,但是可以延迟初始化。
组件可以在子线程中初始化。

在进行优化的时候,需要注意以下几种情况:

放在子线程的组件初始化建议延迟初始化,这样就可以了解是否会对项目造成影响!
将需要在主线程中初始化但是可以不用立即完成的动作延迟加载(初始化放在 Application 中统一管理为妙,不建议放在Activity里面)

  handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                // 设置数据库的上下文
                DBOpenHelper.setContext(context);
                // 初始化GreenDao
                initGreenDao();
                // 加载分类数据
                loadClassifyData();

                //百度地图初始化,百度地图需要在Application初始化
                GamActivityMap.init(Kongfz.this);
            }
        }, 3000);

可以尝试将常见的组件库,例如 Bugly,x5内核初始化,SP的读写,友盟等组件放到子线程中初始化。(子线程初始化不能影响到组件的使用)

 new Thread() {
            @Override
            public void run() {
                super.run();
                //设置线程的优先级,不与主线程抢资源
                setPriority(1);
                //子线程初始化第三方组件
                try {
                    Thread.sleep(5000);//建议延迟初始化,可以发现是否影响其它功能,或者是崩溃!
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //配置umeng、百度统计信息
                configAnalytic();
                //ali 实人认证sdk初始化
                RPSDK.initialize(getContext());
                //手机内存初始化
                Util.getMemoryLevel(getContext());
                //第三方分享
                ShareUtil.shareConfig(getContext());
                //听云
                TingYuntils.init(Kongfz.this);


                LogUtil.e("app", "onCreate Application");
                LogUtil.e("app", "width=" + Util.getScreenWidth(getContext()) + ";height=" + Util.getScreenHeight(getContext()) + ";statusBarHeight=" + Util.getStatusBarHeight(getContext()) + ";dpi=" + Util.getDensityDpi(getContext()));

                //推送初始化
                Config.setMiniTest();

                //推动初始化
                PushMainUtils.initMergePushSDk(true, Kongfz.this);
            }
        }.start();

在优化好启动时间后,我们就可以在针对闪屏页的时间,进行调整优化,具体公式为:

闪屏页展示总时间 = 组件初始化时间 + 剩余展示时间

4.4 优化启动页Activity

启动页尽量不要网络请求等耗时操作。如果使用了请求网络等操作在适当的时候应该及时取消的耗时操作。例如,某些时候,当用户点击了launcher图标,但马上又想退出点击了返回键,过了几秒钟用户在使用其他APP,突然跳转到我们的APP那就用户体验感很不好了。所以可以在返回事件中取消掉耗时操作。
减少欢迎页复杂逻辑,复杂布局,onCreate()中过多逻辑。

4.5 开启服务做处理

可以在后台处理的,放在后台处理,而不用立马处理。

4.6 MultiDex.install(this);优化 当项目方法超过65535,需要分包加载,Android5.0默认实现,Android5.0没有实现,可能出现崩溃,或者第一次加载缓慢的情况。具体可以参考下面文章, 有详细原理分析,以及解决

https://juejin.im/post/5d95f4a4f265da5b8f10714b

五、启动页适配

启动页面适配对于Android手机来说是一个很麻烦的问题,刘海屏、底部虚拟导航栏、各种尺寸的手机屏幕,导致启动页被拉伸变形等出现,那么怎么进行启动页适配呢?下面说两种比较通用的适配方案:
5.1 切目前主流手机尺寸放入不同资源文件夹中进行适配:


资源示例

此种方式问题:
1.适配不能完全适配只能适配主流的机型。
2.文件资源过多,导致包变大。
5.2 切部分图进行适配。

六、优化效果

OPPO优化前:

E:\code\kfz-android>adb shell am start -S -W com.xxx.app/com.xxx.app.xxx.SplashActivity
Stopping: com.xxx.app
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.xxx.app/.xxx.SplashActivity }
Status: ok
Activity: com.xxx.app/.xxx.SplashActivity
ThisTime: 1723
TotalTime: 1723
WaitTime: 1761
Complete

OPPO优化后:

E:\code\kfz-android>adb shell am start -S -W com.xxx.app/com.xxx.app.xxx.SplashActivity
Stopping: com.xxx.app
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.xxx.app/.xxx.SplashActivity }
Status: ok
Activity: com.xxx.app/.xxx.SplashActivity
ThisTime: 717
TotalTime: 717
WaitTime: 733
Complete

华为优化前:

E:\code\KongfzCode2\kfz-android>adb shell am start -S -W com.xxx.app/com.xxx.app.xxx.SplashActivity
Stopping: com.xxx.app
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.xxx.app/.xxx.SplashActivity }
Status: ok
Activity: com.xxx.app/.business.SplashActivity
ThisTime: 2613
TotalTime: 2613
WaitTime: 2644
Complete

华为优化后:

E:\code\kfz-android>adb shell am start -S -W com.xxx.app/com.xxx.app.xxx.SplashActivity
Stopping: com.kongfz.app
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.xxx.app/.xxx.SplashActivity }
Status: ok
Activity: com.xxx.app/.xxx.SplashActivity
ThisTime: 1632
TotalTime: 1632
WaitTime: 1660
Complete

你可能感兴趣的:(Android 启动优化)