如果你已经有 2 - 3 年以上开发经验还不懂的怎么去优化自己的项目,那就有点说不过去了,下面是我自己总结的一套通用级别的 Android 性能优化。
程序员:
之前做热修复的时候研究过 Application 的启动原理。项目中也做过一些启动优化。
面试官:
哦,你之前研究过热修复? (这个时候有可能就会深入的问问热修复的原理,这里咱们就不讨论热修复原理) 那你说说对启动方面都做了哪些优化?
程序员:
windowBackground
,由此推断就是这个属性捣的鬼,开始我是通过设置 windowIsTranslucent
透明属性,发现虽然没有了白屏,但是中间还是有一小段不可见,这个用户体验还是不好的。最后我观察了市面上大部分的 Android 软件在冷启动的时候都会有一个 Splash
的广告页,同时在增加一个倒数的计时器,最后才进入到登录页面或者主页面。我最后也是这样做的,原因是这样做的好处可以让用户先基于广告对本 APP 有一个基本认识,而且在倒数的时候也预留给咱们一些对插件和一些必须或者耗时的初始化做一些准备。Ps:这里会让面试官感觉你是一个注重用户体验的
attachBaseContext
、onCreate
生命周期,由此可见我们不能在这 2 个生命周期里做主线程耗时操作。Ps: 这里会让面试官感觉你对 App 应用的启动流程研究的比较深,有过真实的翻阅底层源码,而并不是背诵答案。
知道了 attachBaseContext
、onCreate
在应用中最先启动,那么我们就可以通过 TreceView 等性能检测工具,来检测具体函数耗时时间,然后来对其做具体的优化。
MultiDex
优化,在第一次启动的时候,直接加载没有经过 OPT 优化的原始 DEX,先使得 APP 能够正常启动。然后在后台启动一个单独进程,慢慢地做完 DEX 的 OPT 工作,尽可能避免影响到前台 APP 的正常使用。Ps:1. 面试官这里会觉得你对启动优化确实了解的不错,有一定的启动优化经验。
- 在第五点面试官会觉得你比较关注该圈子的动态,发现好的解决方案,并能用在自己项目上。这一点是加分项!
onCreate
、onStart
、onRemuse
函数,那么这里由于 onCreate 生命周期中如果调用了 setContentView
函数,底层就会通过将 XML2View 那么这个过程肯定是耗时的。所以要精简 XML 布局代码,尽可能的使用 ViewStub
、include
、merge
标签来优化布局。接着在 onResume 声明周期中会请求 JNI 接收 Vsync (垂直同步刷新的信号) 请求,16ms 之后如果接收到了刷新的消息,那么就会对 DecorView 进行 onMeasure->onLayout->onDraw
绘制。最后才是将 Activity 的根布局 DecorView 添加到 Window 并交于 SurfaceFlinger 显示。所以这一步除了要精简 XML 布局,还有对自定义 View 的测量,布局,绘制等函数不能有耗时和导致 GC 的操作。最后也可以通过 TreaceView
工具来检测这三个声明周期耗时时间,从而进一步优化,达到极限。
这一步给面试官的感觉你对整个 Activity 的启动和 View 的绘制还有刷新机制都有深入的研究,那么此刻你肯定给面试官留了一个好印象,说明你平时对这些源码级别的研究比较广泛,透彻。
总结:
最后我基于以上的优化减少了 50% 启动时间。
面试官:
嗯,研究的挺深的,源码平时不少看吧。
程序员:
到这里,我知道这一关算是过了!
程序员:
有做过,目前的项目内存优化还是挺多的,要不我先说一下优化内存有什么好处吧?咱们不能盲目的去优化!
有的时候对于自己熟悉的领域,一定要主动出击,自己主导这场面试。
面试官:
可以。
Ps:这里大多数面试官会同意你的请求,除非遇见装B的。
程序员:
好处:
那么我基于这四点,我的程序做了如下优化:
在应用开发阶段我比较喜欢用 LeakCanary 这款性能检测工具,好处是它能实时的告诉我具体哪个类发现了内存泄漏(如果你对 LeakCanary 的原理了解的话,可以说一说它是怎么检测的)。
还有我们要明白为什么应用程序会发送 OOM ,又该怎么去避免它?
发生 OOM 的场景是当申请 1M 的内存空间时,如果你要往该内存空间存入 2M 的数据,那么此时就会发生 OOM。
在应用程序中我们不仅要避免直接导致 OOM 的场景还要避免间接导致 OOM 的场景。间接的话也就是要避免内存泄漏的场景。
内存泄漏的场景是这个对象不再使用时,应用完整的执行最后的生命周期,但是由于某些原因,对象虽然已经不再使用,仍然会在内存中存在而导致 GC 不会去回收它,这就意味着发生了内存泄漏。(这里可以介绍下 GC 回收机制,回收算法,知识点尽量往外扩展而不脱离本题)
最后在说一下在实际开发中避免内存泄漏的场景:
其实这些都是基础,把它记下就行了。记得多了在实际开发中就有印象了。
2.减少卡顿
怎么减少卡顿? 那么我们可以从 2 个原理方面来探讨卡顿的根本原因,第一个原理方面是绘制原理,另一个就是刷新原理。
View 的 requestLayout 和 ViewRootImpl##setView 最终都会调用 ViewRootImpl 的 requestLayout 方法,然后通过 scheduleTraversals 方法向 Choreographer 提交一个绘制任务,然后再通过 DisplayEventReceiver 向底层请求 vsync 垂直同步信号,当 vsync 信号来的时候,会通过 JNI 回调回来,在通过 Handler 往消息队列 post 一个异步任务,最终是 ViewRootImpl 去执行绘制任务,最后调用 performTraversals 方法,完成绘制。
详细流程可以参考下面流程图:
卡顿的根本原因:
从刷新原理来看卡顿的根本原理是有两个地方会造成掉帧:
一个是主线程有其它耗时操作,导致doFrame 没有机会在 vsync 信号发出之后 16 毫秒内调用;
还有一个就是当前doFrame方法耗时,绘制太久,下一个 vsync 信号来的时候这一帧还没画完,造成掉帧。
既然我们知道了卡顿的根本原因,那么我们就可以监控卡顿,从而可以对卡顿优化做到极致。我们可以从下面四个方面来监控应用程序卡顿:
//1\. 开启监听
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
//2\. 只要分发消息那么就会在之前和之后分别打印消息
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
...
for (;;) {
Message msg = queue.next(); // might block
...
//分发之前打印
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
...
try {
//分发消息
msg.target.dispatchMessage(msg);
...
//分发之后打印
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
}
}
基于开源框架 BlockCanary 来监控
基于开源框架 rabbit-client 来监控
怎么避免卡顿:
一定要避免在主线程中做耗时任务,总结一下 Android 中主线程的场景:
还有一个最重要的就是避免内存抖动,不要在短时间内频繁的内存分配和释放。
基于这几点去说卡顿肯定是没有问题的。
可以从如下几个方面去展开说明:
1. AutoBoxing(自动装箱): 能用小的坚决不用大的。
2. 内存复用
3. 使用最优的数据类型
4. 枚举类型: 使用注解枚举限制替换 Enum
5. 图片内存优化(这里可以从 Glide 等开源框架去说下它们是怎么设计的)
1. 选择合适的位图格式
2. bitmap 内存复用,压缩
3. 图片的多级缓存
6. 基本数据类型如果不用修改的建议全部写成 static final,因为 它不需要进行初始化工作,直接打包到 dex 就可以直接使用,并不会在 类 中进行申请内存
7. 字符串拼接别用 +=,使用 StringBuffer 或 StringBuilder
8. 不要在 onMeause, onLayout, onDraw 中去刷新 UI
9. 尽量使用 C++ 代码转换 YUV 格式,别用 Java 代码转换 RGB 等格式,真的很占用内存
减少程序异常那么我们可以从稳定性和 Crash 来分别说明。
这个我们将在第四点会详细的介绍程序的稳定性和 Crash 。
如果说出这些,再实际开发中举例说明一下怎么解决的应该是没有问题的。
程序员:
有遇见, 比如在主线程中做耗时操作、频繁的创建对象和销毁对象导致 GC 回收频繁、布局的层级多等。
面试官:
嗯,那具体说说是怎么优化的。
程序员:
这里我们还是可以从显示原理和优化建议来展开说明,参考如下:
1. 显示原理:
View 的 requestLayout 和 ViewRootImpl##setView 最终都会调用 ViewRootImpl 的 requestLayout 方法,然后通过 scheduleTraversals 方法向 Choreographer 提交一个绘制任务,然后再通过 DisplayEventReceiver 向底层请求 vsync 垂直同步信号,当 vsync 信号来的时候,会通过 JNI 回调回来,在通过 Handler 往消息队列 post 一个异步任务,最终是 ViewRootImpl 去执行绘制任务,最后调用 performTraversals 方法,完成绘制。
详细流程可以参考下面流程图:
从刷新原理来看卡顿的根本原理是有两个地方会造成掉帧:
一个是主线程有其它耗时操作,导致doFrame 没有机会在 vsync 信号发出之后 16 毫秒内调用;
还有一个就是当前 doFrame 方法耗时,绘制太久,下一个 vsync 信号来的时候这一帧还没画完,造成掉帧。
既然我们知道了卡顿的根本原因,那么我们就可以监控卡顿,从而可以对卡顿优化做到极致。我们可以从下面四个方面来监控应用程序卡顿:
1. 基于 Looper 的 Printer 分发消息的时间差值来判断是否卡顿。
//1\. 开启监听
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
//2\. 只要分发消息那么就会在之前和之后分别打印消息
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
...
for (;;) {
Message msg = queue.next(); // might block
...
//分发之前打印
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
...
try {
//分发消息
msg.target.dispatchMessage(msg);
...
//分发之后打印
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
}
}
2. 基于 Choreographer 回调函数 postFrameCallback 来监控
3. 基于开源框架 BlockCanary 来监控
4. 基于开源框架 rabbit-client 来监控
怎么可以提高程序运行流畅
1.布局优化:
1.1 布局优化分析工具:
**1.2 优化方案**:
![](https://img-blog.csdnimg.cn/img_convert/4d2c9161398c4a9b6a3af0dc4def0eeb.png)
提升动画性能
怎么避免卡顿:
一定要避免在主线程中做耗时任务,总结一下 Android 中主线程的场景:
基于这几点去说卡顿肯定是没有问题的。
程序员:
保证程序的稳定我们可以从内存、代码质量、Crash、ANR、后台存活等知识点来展开优化。
面试官:
那你具体说说你是怎么做的?
程序员:
1.内存
可以从第二点内存优化来说明
2.代码质量
3. Crash
4. ANR
5. 后台存活
面试官:
嗯,你对知识点掌握的挺好。
说完这些,这一关也算是过了。
程序员:
有,这一点其实可以通过 OKHTTP 连接池和 Http 缓存来说一下(当然这里不会再展开分析 OKHTTP 源码了)
面试官:
那你具体说一下吧
程序员:
说了这些之后,再说一下你当前使用网络框架它们做了哪些优化比如 OKHTTP(Socket 连接池、Http缓存、责任链)、Retrofit(动态代理)。说了这些一般这关也算是过了。
程序员:
主要用过 sp,File,SQLite 存储方式。其中对 sp 和 sqlite 做了优化。
面试官:
那你说说都做了哪些优化?
程序员:
这一块如果你使用过其它第三方的数据库,可以说说它们的原理和它们存取的方式。
程序员:
有做过。比如重复绘制,还有大图长图有过优化。
面试官:
那具体说一说
程序员:
最后也是结合真实场景具体说一个。
程序员:
在没有优化之前持续工作 30 分钟的耗电量是 8%, 优化后是 4%。
面试官:
那你说一说你是怎么优化的。
程序员:
因为我们产品是一款社交通信的软件,有音视频通话、GPS 定位上报、长连接的场景,所以优化起来确实有点困难。不过最后也还是优化了一半的电量下去。主要做了如下优化:
说出这些之后,在结合项目一个真实的优化点来说明一下。
程序员:
有优化,在之前没有考虑任何性能的情况下,我是直接有 log 就写入文件,尽管我开了线程池去写文件,只要软件在运行那么就会频繁的使 CPU 进行工作。这也间接的导致了耗电。
面试官:
那你具体说一下,最后怎么解决这个问题的?
程序员:
展开上面这些点说明之后,面试官一般不会为难你。
程序员:
有过优化,在没有优化之前项目的包体积大小是 80M,优化之后是 50M.
面试官:
说一说是怎么优化的
程序员:
基于这几点优化方案,一般都能解决 APK 体积问题。最后再把自己项目 APK 体积优化步骤结合上面点说一下就行。
其实性能优化点都是息息相关的,比如卡顿会涉及内存、显示,启动也会涉及 APK dex 的影响。所以说性能优化不仅仅是单方面的优化,一定要掌握最基本的优化方案,才能更加深入探讨性能原理问题。
在这里也建立大家多看流行开源框架源码,比如 Glide (内存方面), OKhttp (网络连接方面) 优化的真的很极致。到这里性能优化方面的知识也就说完了,下来一定好好去消化。
为了帮助到大家更好的掌握性能优化相关知识点,这准备了 性能优化知识点汇总和Android 性能监控框架 的学习文档,中间记录了 启动优化、内存优化、UI优化……等知识点,有需要的可以 直接通过此处↓↓↓ 进行参考学习!
《Android 性优化手册》
启动优化
UI渲染优化
崩溃优化
内存优化
卡顿优化
存储优化
网络优化
有需要的可以 直接通过此处↓↓↓ 进行参考学习!
耗电优化多线程并发优化
Android 性能监控框架 Matrix