先讲点题外话
简述Activity的几种启动模式
- standard标准启动模式,也是Activity的启动模式,以这种模式启动的Activity会新new一个Activity对象并放入Activity堆栈,在这种模式下允许一个Activity类有多个实例,并且可互相叠加
- singleTop模式,在一个Activity堆栈中允许存在多个实例,比如启动一个Activity,如果该Activity不存在,那么就类似standard模式;如果当前堆栈中已经存在一个Activity实例,但是不在栈顶,那也会新new一个实例,然后put到栈顶;如果当前已经有Activity在栈顶,那就不会再new一个新的Activity,而是直接回调这个Activity的onNewIntent
- singleTask模式,在一个Activity堆栈中只允许存在一个Activity的实例,比如启动一个Activity,如果这个Activity不存在,则跟standard模式一样,生成新的实例,然后put到堆顶;如果这个Activity已经存在于栈中,那么会把该Activity之上的Activity都destroy掉,然后把该Activity显示出来,并回调onNewIntent方法
- singleInstance模式是只允许有一个实例,而且是运行在自己单独的一个Activity堆栈中的,并且这个堆栈只允许有这个Activity,不能有其他的Activity
如何计算Activity启动时间
- 如果你的手机有root过,那么就可以切换到system_process进程查看ActivityManager打印的系统log:
09-15 22:58:51.624 1193-1266/system_process I/ActivityManager: Displayed com.xtc.watch/.view.account.login.activity.WelcomeActivity: +1s150ms (total +4s743ms)
以上打印出了所谓的thisTime和totalTime,thisTime是指当前Activity的启动时间,正常情况下,如果从桌面启动一个Activity,那么thisTime==totalTime,但是通常app会有一个不加载布局文件的闪屏页面,然后再跳转到相应的Activity,这时候thisTime仅仅是代表最后一个Activity的启动时间,而totalTime还包括而totalTime是指APP进程启动时长,闪屏页面的启动时长以及闪屏页面的消失,新Activity的启动时长之和,所以关注APP的启动时间,我们通常关注的是totalTime
- 通过程序打印log来计算启动时长,在Application的onCreate方法的第一句开始计算,然后到进入指定Activity的onWindowFocus里面停止计算,这之间的时间差就是启动耗时
TraceView识别耗时方法
-
对于APP启动来说,启动耗时包括Android系统启动APP进程加上APP启动界面的耗时时长,我们可做的优化是APP启动界面的耗时,也就是说从Application的onCreate到主界面的onWindowFocusChanged的这一段时间,所以我们可以用Debug.startMethodTracing()和Debug.stopMethodTracing()来抓取这段时间内的方法耗时,在手机sd卡根目录会声场一个.trace的文件,用monitor的TraceView打开就能看到以下分析图解:
如上图,纵轴是各个线程,横轴是时间,下半部分是函数执行耗时以及函数堆栈信息,通常分析的时候,我们可以先看下纵轴,跟APP相关的运行线程多不多,如果多的话可能会有线程竞争问题导致主线程卡顿(普通线程的优先级和主线程的优先级是一样的,如果线程开太多的话,cpu可能会挂起主线程而先去执行其他线程),具体再去看函数的调用情况,有几个指标需要注意下:
- Incl Cpu Time指的是函数执行占用cpu的总时间的百分比和总时间,这里指的总时间是包括函数里面所有调用子函数的耗时之和
- Excl Cpu Time指的是单个函数执行占用cpu的时间,例如函数a()里面又调用了函数b() ,c(),d(),这里的时间仅仅是a的执行时间和不包括b,c,d的耗时
- Incl Real Time是函数实际运行的总时间
- Excl Real Time是函数自己实际运行的时间,不包括该函数体内调用的其他子函数的运行时间
- Call是函数被调用的次数,如果一个函数被调用的非常多次,那说明这里耶可能存在异常
- 函数调用次数和cpu time以及real time的一些比例信息,这些指标可以看出一个函数的平均每次执行的耗时信息
以上的这些指标都可以排序,而我们就可以很方便的查看到底哪些方法耗时最长,哪些方法被调用次数最多,哪些方法平均耗时最大,函数堆栈信息还可以查看函数的Parant和Children,Parent代表这个函数的父函数,也就是说这个函数被包括在哪一个方法里面,children是指这个函数体里面又调用了哪些方法,可以一层一层的跟踪下去
Systreace识别主线程卡顿问题,View的加载情况
- 用Trace.beginSection和Trace.endSection来抓取这之间的一些信息,具体是用Monitor去抓取一段时间内的trace信息,然后会在指定的目录生成一个html文件,用谷歌浏览器输入chrome://tracing,然后load这个html文件就可以把信息可视化,就像下面一样:
通常我们先看Alert信息,这里面就是一些警告信息,给你一些提示,在systrace的分析文件中,可以看到有问题的Frame(红色代表严重,黄色代表警告,绿色代表正常),点击有问题的Frame,可以具体放大查看这个Frame都做了一些什么事情:
从图中可以看到,一个View的measure,layout的耗时情况,inflat xml文件耗时等等,这样很容易就找到具体哪一个View加载耗时最长,而且这个View的加载耗时都是消耗的哪一个过程中(inflat,measure,layout,draw),还可以看到加载图片的耗时,问题点找到了,我们就能针对性的去做优化了,例如xml布局扁平化,ViewStub的使用,降低图片的分辨率,做图片缓存,图片缩放,设置工作线程的优先级为后台的优先级,避免和主线程产生大量的线程进程等等这些问题,总之明确一点:只要问题找到了,那么改起来就简单了,关键是问题的分析过程;Systrace还可以查看UI Thread的执行情况,在哪一个时段是处于Running状态,在哪一个时段是出于Sleeping状态,如果UI Thread出于Sleeping状态,那么在这个时间段内cpu是在执行什么线程,我们就可以考虑是不是可以把这个工作线程延迟执行,这样就能尽可能保证UI Thread大多是Running状态,而不是断断续续的,因为frame的刷新频率一旦低于16ms,那么我们肉眼就能感觉到界面卡顿,这是一个很不好的体验,降低卡顿就应该尽量保证frame的刷新频率控制在16ms以内,所以这就要求在准备frame的工作执行不能超过16ms
造成启动速度慢的常见原因
- 在Application的onCreate里面做了太多的初始化操作,例如第三方库的初始化,其实很多第三方库并不是APP启动了就马上需要初始化,我们完全可以用懒加载的方式,等用到了再去初始化也不迟
- 过于复杂的功能逻辑初始化操作,例如账户登陆需要去进行网络请求验证密码,验证通过后再去服务器拉去一大串的账户数据,然后再通过json解析,保存数据库,再刷新到界面,在APP启动的时候,我们可以先从数据库去搜索数据,把界面线显示出来,然后再去请求网络更新数据
- Activity布局层次嵌套过深,xml布局嵌套过深灰导致加载这个布局的时长加大,因为xml布局的绘制是不断的递归遍历到各个View的根结点,保证扁平化的布局可以有效的缩短布局加载时间;使用ViewStub,因为ViewStub只要你不调用inflat,它是不会去加载View的,在Activity启动后,并不是每一个View都需要马上加载,有一些View根本是GONE,这些View完全可以用ViewStub来实现,等用到的时候再去inflat即可;
- UI线程执行太多耗时操作,数据库操作,文件操作,开过多的线程执行(用RxJava很容易导致这个问题),JSON解析,Bitmap的加载
- Measure/Layout took a significant time, contributing to jank. Avoid triggering layout during animations(避免在View执行动画过程中出发View的layout,否则可能会造成卡顿)
- UI Thread的优先级是默认优先级,而new Thread的优先级也是默认的,所以要是有过多的工作线程可能会造成线程竞争,cpu可能挂起UI Thread而去执行其他的work thread,所以work thread应该设置为background的级别,降低线程竞争的概率
- 在加载View的过程中不要同时去请求数据并更新到View上,在同一时刻做太多的事情也会导致cpu处理不过来而造成卡顿,我们可以等View加载完成之后采取请求数据更新,或者在Activity初始化好了之后再去做其他的数据更新操作(onWindowFocusChanged)
- 误以为在Activity的onResume里面去做一些耗时操作可以优化Activity的启动,事实上Activity在执行到onResume的时候它的初始化操作还没有执行完成呢,如果在这里面执行耗时操作,不会有任何优化效果,应该在Activity第一次被focus的时候(onWindowFocusChanged),这时候Activity已经是完全初始化好了,你可以试下在onWindowFocusChanged去获取View的高度是可以获取到的,但是在onResumen里面去获取View的高度依然还是0
APP闪屏页面实现
- 为了实现点击秒开的效果,我们往往会实现APP闪屏页面,所谓的闪屏页面就是一个不加载布局文件的Activity,但是可以设置它的theme里面的window background成启动欢迎页面(图片分辨率不要太大,否则加载时间会比较长),这样就能达到点击app,马上就能看到启动页面,由于Activity不用setContentView,所以启动闪屏页面的速度也很快,然后再由闪屏页面跳转到欢迎页面,然后再进入主界面,其实这样综合下来,启动时间是变长了,因为在Activity之间切换的时候要先pause上一个activity然后再create下一个Activity,这样会增加一些耗时,不过闪屏页面给用户的是点击了立马就启动APP的感觉,所以即时启动总时长多个两三秒也是可以接受的
Activity的启动流程(具体要开源码)
- Activity或者ContextImpl的startActivity
- Instrumentation的execStartActivity
- ActivityManagerService的startActivity->startActivityAsUser
- ActivityStarter的startActivityMayWait->startActivityLocked->startActivityUnchecked
- ActivityStackSupervisor的resumeFocusedStackTopActivityLocked->resumeFocusedStackTopActivityLocked
- ActivityStack的resumeTopActivityUncheckedLocked->resumeTopActivityInnerLocked
- ActivityStackSupervisor 的startSpecificActivityLocked->realStartActivityLocked
- ActivityManager的scheduleLaunchActivity->handleLaunchActivity->performLaunchActivity->handleResumeActivity到这里一个Activity的启动流程就基本结束,太特么复杂了。。。
- 从Activity的启动流程来分析我们可以得知启动一个Activity需要去匹配到你要启动的Activity(匹配ResolveInfo),这里涉及到显示启动和隐式启动,显示启动的话比较快,不用再去匹配Intent里面的IntentFilter;然后再监测Activity所在的进程是否有启动,没有启动的话就fock一个进程出来接下去再做初始化Application并调用相关方法,例如onCreate,然后通过反射的方式创建Activity对象,再调用Activity的各个生命周期;所以APP的启动时间是包括APP进程启动时长(无法优化),Application的执行时间和Activity的执行时间(这两部分是可以优化的),另外在启动Activity之前会设置Theme,这里可能也会造成耗时,例如theme里面设置了一张分辨率较高的background会导致decode这张图片的时间变长