性能分析工具
在Android开发中,我们经常会遇到一些偶发问题,比如:无响应,应用退出,卡顿等。这些问题我们可以通过日志追踪,当然尽可能不让出现此类问题,这就需要在开发中及时发现和优化有风险的代码。除了利用一些工具检查以外,还要对代码质量有所提高,因此性能优化不是一朝一夕的事。本文主要对绘制,内存,稳定性以及安装包方面进行优化讲解,参考了一些博文和书籍,整理如下。
Android应用启动慢,使用经常卡顿,按场景可以分成如下图四类,这四类场景又可以分成两大类。
Android的显示过程可以简单概括为:应用层把经过测量,布局,绘制后的surface缓存数据,通过SurfaceFlinger把数据显示到屏幕上。应用层负责绘制,系统层负责渲染,通过进程间通信把应用层需要绘制的数据传递到系统层。
应用层:
系统层:
绘制过程是CPU准备数据,通过Driver层把数据交给CPU渲染,其中CPU主要负责Measure,Layout,Record,Execute的数据计算工作,GPU负责Rasterization(栅格化)渲染。CPU和
GPU通信是通过图形驱动层(Graphics Driver)来连接两部分。图形驱动维护了一个队列CPU把display list添加到队列中,GPU从这个队列中取出数据进行绘制,最终显示出来。
FPS(Frames Per Second):每秒传递的帧数,理想状态下,60FPS就感觉不到卡顿。Android系统每隔16ms发出VSYNC信号,这就意味着每个绘制时长应该在16ms以内。
卡顿的根本原因:
主线程要做的工作:
除了这些以外,尽量避免将其他处理放到主线程中,特别是复杂的数据计算和网络请求。
2,绘制分析工具
也可以看看这一篇:https://mp.weixin.qq.com/s/7cqwkEPlfyqcM1ZwByLGBA
绘制问题在某些情况下肉眼不容易发现,尽管能察觉到页面卡顿,也需要借助工具分析问题。
①,GPU呈现模式分析(Profile GPU rendering)
Profile GPU rendering是Android4.1系统开始提供的一个开发辅助功能。
②,过度绘制监测
过度绘制是指在屏幕上的某个像素在同一帧的时间内被绘制了多次。在多次重叠的UI结构(如带背景的TextView)中,如果不可见的UI也在做绘制的操作,就会导致某些像素区域被绘制了多次,从而浪费多余的CPU以及GPU资源。
开发者选项中打开“调试GPU过度绘制”,如右图会在UI上呈现不同的颜色。
③,如何避免过度绘制
①,布局上优化,可以使用Hierarchy View,在AS3.0以上版本在Tools中使用Layout Inspector工具查看层级情况,尽量减少层级数量。还有使用XML布局时,会设置很多背景,如果不必要,尽量移除。
布局优化总结几点如下:
②,自定义View优化,在自定义View中可以通过canvas.clipRect()来帮助系统识别哪些区域可见。这样只有这个区域内才会被绘制,起到节约CPU与GPU资源。
3,布局优化工具(Layout Inspector)
AS3.0以后版本工具,操作步骤:
布局检查器会捕获快照,将它保存为 .li 文件并打开。布局检查器将显示以下内容:
Layout Inspector主要用分析布局的层级结构,减少不必要的层级,避免overdraw,达到渲染优化的效果。虽然界面不如HierarchyView直观,但是提供的信息也足够详细,分析布局层级绝对够用了。
4,布局层级检查
Android Lint是Android SDK Tools 16之后引入的代码检查工具,通过代码静态检查,可以发现潜在的代码问题,并给出优化建议。
检查结果分为6类:
在AS中启动Lint,可以从菜单栏选择Analyze->Inspect Code,进行全面扫描。问题的严重程度从高到低依次:
对于Fatal和Error级别的问题一定要进行优化处理。另外在推荐两个代码约束检查插件FindBug(Java维度的),阿里约束插件。
5,布局优化方法
布局的好坏会影响绘制的时间,减少Layout层级,减少测量,提高复用性。
内存问题也是优化中的重重一个环节,内存占用情况也直接影响APP的性能,做好内存优化刻不容缓。
1,对象生命周期
①,创建阶段(Created)
②,应用阶段(InUse)
对象至少被一个强引用(Strong Reference)持有,除非在系统中显式地使用了软引用(Soft
Reference),弱引用(Weak Reference)或虚引用(Phantom Reference)。
③,不可见阶段(Invisible)
处于不可见阶段的对象在虚拟机的对象引用根集合中再也找不到直接或间接地强引用,这些对象一般是所有线程栈中的临时变量。所有已经装载的静态变量或者是对本地代码接口的引用。
④,不可达阶段(Unreachable)
对象处于不可达阶段时指该对象不再被任何强引用持有,回收器发现该对象已经不可达。
⑤,收集阶段(Collected)
当垃圾回收器发现该对象已经处于“不可达阶段”并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,对象进入“收集阶段”。如果该对象已经重写了finalize()方法,则执行该方法的操作。
⑥,终结阶段(Finalized)
当对象执行完finalized()方法后仍然处于不可达状态时,该对象进入终结阶段。在该阶段,等待垃圾回收器回收该对象空间。
⑦,对象空间重新分配阶段(Deallocated)
若垃圾回收器对该对象占用的内存空间进行回收或者再分配,则该对象彻底消失,这个阶段称为“对象空间重新分配阶段”。
注意: 创建对象后,在确定不需要使用该对象时,使对象置空,这样更符合垃圾回收标准。比如Object=null,可以提高内存使用效率,并且不要采用过深的集成层次。访问本地变量优于访问类中的变量。
2,内存泄漏
在我们了解了内存分配和虚拟机GC原理后,我们知道程序运行过程中,虚拟机会对内存进行回收。内存泄漏的出现就是有一部分内存不能进行回收,也就是指没有用的对象到GC Roots是可达的(对象被引用),导致GC无法回收该对象。
①,WebView造成的泄漏
不同安卓版本的WebView会有差异,加上不同厂商的定制ROM的WebView的差异,这就导致WebView存在很大的兼容性问题。WebView都会存在内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉。通常的解决办法就是为WebView单开一个进程,使用AIDL与应用的主进程进行通信。WebView进程可以根据业务需求,在合适的时机进行销毁。
②,资源对象未关闭
资源对象比如Cursor、File等,往往都用了缓冲,不使用的时候应该关闭。把引用置为null,而不关闭它们,往往会造成内存泄漏。因此,在资源对象不使用时,一定要确保它已经关闭,通常在finally语句中关闭,防止出现异常时,资源未被释放的问题。
③,集合中的对象没清理
通常把一些对象的引用加入到了集合中,当不需要该对象时,如果没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就会更加严重。
④,Bitmap对象
临时创建的某个相对比较大的bitmap对象,在经过变换得到新的bitmap对象之后,应该尽快回收原始的bitmap,这样能够更快释放原始bitmap所占用的空间。 避免静态变量持有比较大的bitmap对象或者其他大的数据对象,如果已经持有,要尽快置空该静态变量。
⑤,监听器未关闭
很多系统服务需要register和unregister监听器,我们需要确保在合适的时候及时unregister那些监听器。自己手动add的Listener,要记得在合适的时候及时remove这个Listener。
⑥,非静态内部类的静态实例
非静态内部类会持有外部类实例的引用,如果非静态内部类的实例是静态的,就会间接的长期维持着外部类的引用,阻止被系统回收。
⑦,Handler内存泄漏
Handler的Message被存储在MessageQueue中,有些Message并不能马上被处理,它们在MessageQueue中存在的时间会很长,这就会导致Handler无法被回收。如果Handler 是非静态的,则Handler也会导致引用它的Activity或者Service不能被回收。
3,内存泄漏监控
对于内存泄漏的问题,一般情况下不容易察觉,也不会有多大问题。但是如果内存泄漏场景多,那就需要考虑了。我们可以使用LeakCanary对内存泄漏现象进行监控。
①,LeakCanary使用:
这是经典的单利模式,第一个方法会存在内存泄漏风险。使用LeakCanary我们可以模拟内存泄漏,在demo中有一个Activity调用第一个方法,当退出Activity时就会出现内存泄漏。
这就是出现内存泄漏的日志,并且LeakCanary还会保存hprof文件助于我们分析问题。
使用MAT分析内存泄漏,LeakCanary生成的hprof文件需要转换才能被MAT工具打开,这里可以使用AS工具转换。
②,MAT基本概览:
显示了堆快照文件的大小、类、实例和ClassLoader的总数饼图中显示了当前堆快照中最大的对象。将鼠标悬停在饼图中,可以在左侧的Inspector界面中,查看该对象的相应信息在饼图中单击某对象,可以对选中的对象进行更多的操作。
Actions一栏的下面列出了MAT提供的四种Action,其中分析内存泄漏最常用的就是Histogram和Dominator Tree。我们点击Actions中给出的链接或者在MAT工具栏中就可以打开Dorminator Tree和Histogram。
Histogram(直方图)
可以通过Histogram分析,Histogram列出了每个类的实例数量,点击Action下的Histogram,得到以下结果:
在这里搜索了SecondActivity,程序中写了一个出现内存泄漏的程序。在某一项上右键打开菜单选择 list objects ->with incoming refs 将列出该类的实例。对于给定一个对象,通过MAT可以找到引用当前对象的对象,即入引用(Incomming References),以及当前对象引用的对象,即出引用(Outgoing References)。
快速找出某个实例没被释放的原因,可以右健 Path to GC Roots–>exclue all phantom/weak/soft etc. reference。这里可以看到TestManager这个类的引用没有被释放。
Dominator Tree(支配树)
Dorminator Tree意味支配树,从名称就可以看出Dorminator Tree更善于去分析对象的引用关系。Dominator Tree提供了一个列表。Dominator Tree:对象之间dominator关系树。如果从GC Root到达Y的的所有path都经过X,那么我们称X dominates Y,或者X是Y的Dominator 。Dominator Tree由系统中复杂的对象图计算而来。从MAT的dominator tree中可以看到占用内存最大的对象以及每个对象的dominator,如下所示:
对于内存抖动问题可以看这个案例。
4,内存空间优化
没有内存泄漏,并不意味着内存就不需要优化了,Android系统对每个应用进程分配了有限的堆内存,因此使用最小内存的对象或者资源可以减小内存开销,同时让GC能更高效地回收不再需要使用的对象,让应用堆内存保持充足的可用内存,使应用更稳定高效地运行。
①,对象引用
合理的利用对象引用,有助于优化内存空间占用。
②,减小不必要的内存开销
稳定性优化其实是指编码本身的优化,不管任何语言,都有一些规范或者缺点。往往规范性的约定就是为了避免一些问题的出现,尤其是代码质量问题。比如同样是一个算法,可能有的效率高。这就需要我们不断的去发现和总结一些语言的特性和规范。
1,代码审查
代码审查在团队开发中也是重要的一环,有些公司会有代码评审,审核不通过不能提交。这些都是为了提升代码质量和程序的稳定性。
①,工具审查
使用Lint代码检查,另外推荐使用FindBugs工具检查Java代码。
②,人工审查
人工审查就需要制定一定的方向,如下:
③,Log审查
按时审查云端log,针对异常上报和Anr问题。
2,ANR问题
一般情况下应用无响应的时候产生一个日志文件,位于/data/anr/文件夹下面,trace文件是Android Davik虚拟机在收到异常终止信号时产生的,最常见的一个触发条件就是Android应用中产生了FC(force close)。由于该文件的产生是在DVM中的,所以只有运行DVM实例的进程才能产生该文件,也就是说只有Java代码才能产生该文件,App应用的Native层(如Android Library、用c/c++编译的库)即使异常也不会产生ANR日志文件。我们可以通过ANR产生的traces日志文件分析应用在哪里产生了ANR,以此来有效解决应用中的ANR。
为什么会产生ANR
在Android里,应用程序的响应是由ActivityManager和WindowManager服务系统服务监视的,当检测到下面三种情况的任何一种时,Android就会针对特定的应用程序显示ANR对话框。
造成ANR的原因有很多,无论是在Activity或者BroadcastReceiver还是在Service,我们看到都是在主线程中操作引起的ANR,因此我们应该避免在主线程做太多耗时的操作,网络请求不用说了,Android4.0以后就禁止在主线程成执行请求了,除此之外就是要注意如下几个方面:
一般traces.txt日志输出格式如下,本实例是在主线程中强行Sleep导致的ANR日志:
第1行是固定头,指明下面都是当前运行的dvm thread:“DALVIK THREADS”。
第2行输出的是改进程中各线程互斥量的值,有些手机上面可能没有这一行日志信息。
第3行输出的是线程名字(“main”),线程优先级(“prio=5”),线程id(“tid=1”),线程状态(Sleeping),比较常见的状态还有Native、Waiting。
第4行分别是线程所处的线程组(“main”),线程被正常挂起的次(“sCount=1”),线程因调试而挂起次数(”dsCount=0“),当前线程所关联的java线程对象(”obj=0x73f11000“)以及该线程本身的地址(“0xf3c25800”)。
第5行 显示线程调度信息,分别是该线程在linux系统下得本地线程id (“sysTid=2957”),线程的调度有优先级(“nice=0”),调度策略(sched=0/0),优先组属(“cgrp=default”)以及处理函数地址(“handle=0xf7770ea0”)。
第6行 显示更多该线程当前上下文,分别是调度状态(从/proc/[pid]/task/[tid]/schedstat读出)(“schedstat=( 107710942 40533261131 )”),以及该线程运行信息 ,它们是线程用户态下使用的时间值(单位是jiffies)(“utm=4”),内核态下得调度时间值(“stm=6”),以及最后运行改线程的cup标识(“core=2”)。
第7行表示线程栈的地址(“stack=0xff49d000-0xff49f000”)以及栈大小(“stackSize=8MB”)。
3,代码优化
对于代码优化和规范约束不同,这需要我们在开发中去总结,针对一些案例去分析。
①,在代码中直接创建新的Thread
new Thread(new Runnable() {
@Override
public void run() {
}
}).start();
这种的做法是非常不可取的,缺点非常的多。浪费线程资源是第一,最重要的是我们无法控制该线程的执行,因此可能会造成不必要的内存泄漏。在Activity或者Fragment这种有生命周期的控件里面直接执行这段代码,相信大部分人都知道会可能有内存泄漏。
推荐做法:利用线程池,利用第三方库如RxJava
②,频繁使用HandlerThread
HandlerThread继承于Thread类,所以每次开启一个HandlerThread就和开启一个普通Thread一样,很浪费资源。
③,Handler规范使用
private BaseHandler mBaseHandler;
/**
* 初始化一个Handler,如果需要使用Handler,先调用此方法,
* 然后可以使用postRunnable(Runnable runnable),
* sendMessage在handleMessage(Message msg)中接收msg
*/
public void initHandler() {
mBaseHandler = new BaseHandler(this);
}
/**
* 返回Handler,在此之前确定已经调用initHandler()
*
* @return Handler
*/
public Handler getHandler() {
return mBaseHandler;
}
/**
* 同Handler的postRunnable
* 在此之前确定已经调用initHandler()
*/
protected void postRunnable(Runnable runnable) {
postRunnableDelayed(runnable, 0);
}
/**
* 同Handler的postRunnableDelayed
* 在此之前确定已经调用initHandler()
*/
protected void postRunnableDelayed(Runnable runnable, long delayMillis) {
if (mBaseHandler == null) initHandler();
mBaseHandler.postDelayed(runnable, delayMillis);
}
protected static class BaseHandler extends Handler {
private final WeakReference mObjects;
public BaseHandler(BaseActivity mPresenter) {
mObjects = new WeakReference(mPresenter);
}
@Override
public void handleMessage(Message msg) {
BaseActivity mPresenter = mObjects.get();
if (mPresenter != null)
mPresenter.handleMessage(msg);
}
}
/**
* 同Handler 的 handleMessage,
* getHandler.sendMessage,发送的Message在此接收
* 在此之前确定已经调用initHandler()
*
* @param msg
*/
protected void handleMessage(Message msg) {
}
④,正确的使用Context
Context应该是每个入门Android的程序员第一个接触的概念,他代表当前上下文的环境,可以用来实现很多功能的调用,语句如下:
//获取资源管理器对象,进而可以访问到例如String,color等资源
Resources resources = context.getResources();
//启动指定的Activity
context.startActivity(new Intent(this, MainActivity.class));
//获取各种系统服务
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
//获取系统文件目录
File internalDir = context.getCacheDir();
File externalDir = context.getExternalCacheDir();
//更多...
可见,正确的理解Context的概念是很重要的,虽然在应用开发中随处可见Context的使用,但并不是所有的Context实力都具有相同的功能,在使用上需要区别对待,否则会很容易引入问题。
Context的种类
根据Context依托的组件以及用途的不同,我们可以将Context分为如下几种。
APK大小优化可以看成是非必须且重要的环节,APK包太大不仅会导致客户的流失,而且也会影响加载。
1, res资源优化
2,代码优化
3,代码混淆
使用proGuard 代码混淆器工具,它包括压缩、优化、混淆等功能。
4,assets资源优化
5,lib资源优化