Android app启动优化

1,592 阅读12分钟

一、应用的启动方式

  通常来说,启动方式分为两种:冷启动和热启动。

  1、冷启动:当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用,这个启动方式就是冷启动。

  2、热启动:当启动应用时,后台已有该应用的进程(例:按back键、home键,应用虽然会退出,但是该应用的进程是依然会保留在后台,可进入任务列表查看),所以在已有进程的情况下,这种启动会从已有的进程中来启动应用,这个方式叫热启动。

  特点

  1、冷启动:冷启动因为系统会重新创建一个新的进程分配给它,所以会先创建和初始化Application类,再创建和初始化MainActivity类(包括一系列的测量、布局、绘制),最后显示在界面上。

  2、热启动:热启动因为会从已有的进程中来启动,所以热启动就不会走Application这步了,而是直接走MainActivity(包括一系列的测量、布局、绘制),所以热启动的过程只需要创建和初始化一个MainActivity就行了,而不必创建和初始化Application,

  因为一个应用从新进程的创建到进程的销毁,Application只会初始化一次。

二、应用的启动过程

  冷启动启动流程:当点击app的启动图标时,安卓系统会从Zygote进程中fork创建出一个新的进程分配给该应用:

  之后会依次创建和初始化Application类、创建MainActivity类、加载主题样式Theme中的windowBackground等属性设置给MainActivity以及配置Activity层级上的一些属性、再inflate布局、当onCreate/onStart/onResume方法都走完了后最后才进行contentView的measure/layout/draw显示在界面上,所以直到这里,

  应用的第一次启动才算完成,这时候我们看到的界面也就是所说的第一帧。所以,总结一下,应用的启动流程如下:

  Application的构造器方法——>attachBaseContext()——>onCreate()——>Activity的构造方法——>onCreate()——>配置主题中背景等属性——>onStart()——>onResume()——>测量布局绘制显示在界面上。  

  大致流程如下:

  1、点击桌面图标,Launcher会启动程序默认的Acticity,之后再按照程序的逻辑启动各种Activity

  2、启动Activity都需要借助应用程序框架层的ActivityManagerService服务进程(Service也是由ActivityManagerService进程来启动的);在Android应用程序框架层中,ActivityManagerService是一个非常重要的接口,

  它不但负责启动Activity和Service,还负责管理Activity和Service。

    Step 1. 无论是通过Launcher来启动Activity,还是通过Activity内部调用startActivity接口来启动新的Activity,都通过Binder进程间通信进入到ActivityManagerService进程中,并且调用ActivityManagerService.startActivity接口;

    Step 2. ActivityManagerService调用ActivityStack.startActivityMayWait来做准备要启动的Activity的相关信息;

    Step 3. ActivityStack通知ApplicationThread要进行Activity启动调度了,这里的ApplicationThread代表的是调用ActivityManagerService.startActivity接口的进程,对于通过点击应用程序图标的情景来说,这个进程就是Launcher了,

    而对于通过在Activity内部调用startActivity的情景来说,这个进程就是这个Activity所在的进程了;

    Step 4. ApplicationThread不执行真正的启动操作,它通过调用ActivityManagerService.activityPaused接口进入到ActivityManagerService进程中,看看是否需要创建新的进程来启动Activity;

    Step 5. 对于通过点击应用程序图标来启动Activity的情景来说,ActivityManagerService在这一步中,会调用startProcessLocked来创建一个新的进程,而对于通过在Activity内部调用startActivity来启动新的Activity来说,这一步是不需要执行的,

    因为新的Activity就在原来的Activity所在的进程中进行启动;

    Step 6. ActivityManagerServic调用ApplicationThread.scheduleLaunchActivity接口,通知相应的进程执行启动Activity的操作;

    Step 7. ApplicationThread把这个启动Activity的操作转发给ActivityThread,ActivityThread通过ClassLoader导入相应的Activity类,然后把它启动起来。 链接        

三、消除启动时的白屏/黑屏

1、将背景图设置成我们APP的Logo图,作为APP启动的引导,现在市面上大部分的APP也是这么做的:

    <style name="AppTheme.NoActionBar">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
    </style>
    
    <style name="AppTheme.NoActionBar.Splash">
        <item name="android:windowBackground">@drawable/splash_image_1</item>
    </style>

2、将背景颜色设置为透明色,这样当用户点击桌面APP图片的时候,并不会"立即"进入APP,而且在桌面上停留一会,其实这时候APP已经是启动的了,只是我们原来黑/白屏windowBackground的颜色设置成透明的:

   <style name="AppTheme.NoActionBar.Transparent">
        <item name="android:windowBackground">@color/transparent</item>
        <item name="android:windowAnimationStyle">@android:style/Animation.Translucent</item>
        <item name="android:windowIsTranslucent">true</item>
   </style>

四、启动耗时统计

adb shell am start -W [-D] [PackageName]/[PackageName.MainActivity] 加-D可等待调试器附加到进程 执行成功后将返回三个测量到的时间:

这里面涉及到三个时间,ThisTime、TotalTime 和 WaitTime。WaitTime 是 startActivityAndWait 这个方法的调用耗时,ThisTime 是指调用过程中最后一个 Activity 启动时间到这个 Activity 的 startActivityAndWait 调用结束。TotalTime 是指调用过程中第一个 Activity 的启动时间到最后一个 Activity 的 startActivityAndWait 结束。如果过程中只有一个 Activity ,则 TotalTime 等于 ThisTime。 总结:如果只关心某个应用自身启动耗时,参考TotalTime;如果关心系统启动应用耗时,参考WaitTime;如果关心应用有界面Activity启动耗时,参考ThisTime。 链接:www.jianshu.com/p/a0e242d57…

五、启动耗时优化

一般而言,热启动时时间较长是因为在MainActivity的onCreate()函数中做了太多UI耗时操作,而冷启动时间较长主要跟Application的onCreate()函数以及MainActivity的onCreate()函数的执行时间有关。 工欲善其事,必先利其器,首先来介绍一下Android测量函数耗时的几个方法:

1、使用Android Studio 的profiler功能:

点击record录制app启动过程中执行每个函数及其子函数的CPU耗时,非常直观的可以找到启动过程中耗时最多的地方。为了捕获到完整的冷启动流程,在开始录制之前先到Android系统设置菜单中选择带调试的应用:

然后点击应用图标,系统会等待我们attach到相应的进程,我们首先开始CPU耗时录制然后在attach到进程,这样子就可以记录到Application onCreate函数的执行耗时了。

要想追踪指定代码段的操作耗时,可以在代码段执行前后跳转追踪调试方法:

Debug.startMethodTracing(“launch”);
....
代码
.....
Debug.stopMethodTracing();

随着程序的运行,在/sdcard/文件夹下会自动创建一个launch.trace的文件,这个文件可以通过Android Studio profiler和eclipse traceView打开,然后进行分析。

2、使用eclipse traceView打开trace文件:

左上角为线程面板,右上角为时间面板,底部是数据分析面板。时间线面板以每个线程为一行,右边是该线程在整个过程中方法执行的情况,一行中有很多的小色块。这些色块代表采集过程中方法调用时间线,相同的颜色代表相同的方法,其中的每一个小色块就代表一次方法的调用,色块的长度代表方法执行时间的长短,左边为第一个色块代表方法执行开始,最右边色块代表最后一个方法执行结束,有时候可以根据色块长度来做个大致判断,哪一个方法执行时间相对来说比较长,你可以把鼠标放到色块上,就会显示该方法调用的详细信息。在数据分析面板,你可以点击某个函数展开更详细的信息,展开后,大多数有以下两个类别:

Parents:调用该方法的父类方法

Children:该方法调用的子类方法

如果该方法含有递归调用,可能还会多出两个类别:

Parents while recursive:递归调用时所涉及的父类方法

Children while recursive:递归调用时所涉及的子类方法

至于数据分析面板红色框中,各个字段的含义如下:

开发者最关心的数据有: 很重要的指标:Calls + Recur Calls / Total , 最重要的指标: Cpu Time / Call 因为我们最关心的有两点,一是调用次数不多,但每次调用却需要花费很长时间的函数。这个可以从Cpu Time / Call反映出来。另外一个是那些自身占用时间不长,但调用却非常频繁的函数。这个可以从**Calls + Recur Calls / Total **反映出来。

3、使用StrictMode线程策略

StrictMode意思为严格模式,是用来检测程序中违例情况的开发者工具,线程策略(ThreadPolicy)检测的内容有

1)自定义的耗时调用 使用detectCustomSlowCalls()开启

2)磁盘读取操作 使用detectDiskReads()开启

3)磁盘写入操作 使用detectDiskWrites()开启

4)网络操作 使用detectNetwork()开启

严格模式的开启可以放在Application或者Activity以及其他组件的onCreate方法。为了更好地分析应用中的问题,建议放在Application的onCreate方法中:

StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                .detectAll()//开启所有的detectXX系列方法
                .penaltyDialog()//弹出违规提示框
                .penaltyLog()//在Logcat中打印违规日志
                .build());

setThreadPolicy()将对当前线程应用该策略。如果不指定检测函数,也可以用detectAll()来替代。penaltyLog()表示将警告输出到LogCat,你也可以使用其他或增加新的惩罚(penalty)函数,例如使用penaltyDeath()的话,一旦StrictMode消息被写到LogCat后应用就会崩溃。另外虚拟机策略(VmPolicy)不能通过一个对话框提供警告。

在线程策略(ThreadPolicy)检测的时候,有几个penalty系列方法。

1)penaltyDeath(),当触发违规条件时,直接Crash掉当前应用程序。

2)penaltyDeathOnNetwork(),当触发网络违规时,Crash掉当前应用程序。

3)penaltyDialog(),触发违规时,显示对违规信息对话框。

4)penaltyFlashScreen(),会造成屏幕闪烁,不过一般的设备可能没有这个功能

4、使用Android Systrace

Systrace 允许在系统级别收集和检查设备上运行的所有进程的计时信息。 它将来自Android内核的数据(例如CPU调度程序,磁盘活动和应用程序线程)组合起来,以生成HTML报告。如果想分析Android系统或者app的问题,首先我们需要抓取Systrace文件分析并找出引起系统卡顿,或者app反应慢的原因,然后在源码上解决引起慢的问题。首先链接手机,打开Android Device Monitor,选择要分析的进程,点击Capture system wide trace using Android。

根据不同的需求,配置抓取不同的trace 时间(时间请勿过长,否则会导致抓取内容部分丢失),内容等,然后点击OK,操作要分析系统卡顿或app运行缓慢的部分,系统会自动收集运行时的信息,然后用Chrome 浏览器打开生成的trace 文件 :

抓取的Trace报告提供了Android系统进程在特定时间段内的整体情况。 它检查捕获的跟踪信息,并突出显示其检查到的问题,里面会包含每个CPU,以及图形渲染,输入事件等等内容,例如在显示运动或动画时UI粗糙,并提供关于如何解决这些问题的建议。 但是,systrace不会在应用程序进程中收集有关代码执行的信息。 有关您的应用程序执行哪些方法以及使用多少CPU资源的更多详细信息,请使用Android Studio的内置Profiler分析器,或生成跟踪日志并使用Traceview查看它们。

六、优化建议

1、页面布局:

1)当根布局文件最外层标签为FrameLayout以及使用include标签引用的布局文件的最外层标签
可以用merge标签来替换,减少一层布局深度。

2)对于页面中不需要立即展示的部件使用ViewStub实现延迟加载的方式

3)合理运用LinearLayout和RelativeLayout,减少布局嵌套,推荐使用ConstraintLayout

4)当时用自定义控件时,onDraw()中及不要做耗时操作

2、应用初始化:

在大部分app开发中,我们都会去重写Application类,然后在onCreate()里进行一些初始化操作,比如创建一些全局单例,第三方SDK以及一些功能模块的初始化等,如果初始化函数耗时过长,应该放到线程池或者intentService中去操作;涉及到数据库和文件等I/O操作,放到真正需要的地方再执行。同样在MainActivity的onCreate()函数中也要注意上述问题,因为只有当Activity生命周期执行到onResume()函数的时候界面才会真正显示出来,onCreate()函数执行时间越长,界面等待显示的时间越长,如果有些初始化流程必须要放在onCreate()当中,可以使用DelayLoad的机制:

getWindow().getDecorView().post(new Runnable() {
            @Override
            public void run() {
                myHandler.post(mLoadingRunnable);
            }
        });

ViewRootImpl 的 performTraversals 方法是一个很核心的方法,每一帧绘制都会走一遍,调用各种 measure / layout / draw 等 ,最终将要显示的数据交给 hwui 去进行绘制。Activity 在启动时,会在第二次执行 performTraversals 才会去真正的绘制,原因在于第一次执行 performTraversals 的时候,会走到 Egl 初始化的逻辑,然后会重新执行一次 performTraversals 。在 onCreate 中 Post 的 runnable 对象,在第一个 performTraversals 方法执行的时候被调用,mLoadingRunnable在界面真正绘制的时候才会执行。