GITHUB
https://blog.51cto.com/6342127/2307514
说明
这篇文章是将很久以来看过的文章,包括自己写的一些测试代码的总结.属于笔记的性质,没有面面俱到,一些自己相对熟悉的点可能会略过.<br>
最开始看到的性能优化的文章,就是胡凯的优化典范系列,后来又陆续看过一些人写的,个人觉得anly_jun和胡凯的质量最好.<br>
文章大的框架也是先把优化典范过一遍,记录个人认为重要的点,然后是anly_jun的系列,将之前未覆盖的补充进去,也包括HenCoder的一些课程相关内容.<br>
当然除了上面几位,还有很多其他大神的文章,时间久了也记不太清,在此一并谢过.
笔记内容引用来源
1.Android性能优化之渲染篇
1.VSYNC
- 帧率:GPU在1秒内绘制操作的帧数.如60fps.
- 我们通常都会提到60fps与16ms,这是因为人眼与大脑之间的协作无法感知超过60fps的画面更新.
- 开发app的性能目标就是保持60fps,这意味着每一帧只有16ms=1000/60的时间来处理所有的任务
- 刷新率:屏幕在1秒内刷新屏幕的次数.如60Hz,每16ms刷新1次屏幕.
- GPU获取图形数据进行渲染,然后屏幕将渲染后的内容展示在屏幕上.
- 大多数手机屏幕的刷新率是60Hz,如果GPU渲染1帧的时间低于1000/60=16ms,那么在屏幕刷新时候都有最新帧可显示.如果GPU渲染某1帧 f 的时间超过16ms,在屏幕刷新时候,f并没有被GPU渲染完成则无法展示,屏幕只能继续展示f的上1帧的内容.这就是掉帧,造成了UI界面的卡顿.
<br>
下面展示了帧率正常和帧率低于刷新率(掉帧)的情形
2.GPU渲染:GPU渲染依赖2个组件:CPU和GPU
- CPU负责Measure,Layout,Record,Execute操作.
- GPU负责Rasterization(栅格化)操作.
- Resterization栅格化是绘制那些Button,Shape,Path,String,Bitmap等组件最基础的操作.它把组件拆分到不同的像素上进行显示.这是一个很费时的操作.
- CPU负责把UI组件计算成Polygons(多边形),Texture(纹理),然后交给GPU进行栅格化渲染.
- 为了App流畅,我们需要确保在16ms内完成所有CPU和GPU的工作.
3.过度绘制
Overdraw过度绘制是指屏幕上的某个像素在同一帧的时间内被绘制了多次.过度绘制会大量浪费CPU及GPU资源/占用CPU和GPU的处理时间
- 过度绘制的原因
- UI布局存在大量重叠
- 非必须的背景重叠.
- 如Activity有背景,Layout又有背景,子View又有背景.仅仅移除非必要背景就可以显著提升性能.
- 子View在onDraw中存在重叠部分绘制的情况,比如Bitmap重叠绘制
4.如何提升渲染性能
- 移除XML布局文件中非必要的Background
- 保持布局扁平化,尽量避免布局嵌套
- 在任何时候都避免调用requestLayout(),调用requestLayout会导致该layout的所有父节点都发生重新layout的操作
-
在自定义View的onDraw中避免过度绘制.
<br>代码实例:<br>效果图:
2.Android性能优化之内存篇
1.Android虚拟机的 分代堆内存/Generational Heap Memory模型
- 和JVM不同:Android的堆内存多了1个永久代/Permanent Generation.
- 和JVM类似:
- 新创建的对象存储在新生代/Young Generation
- GC所占用的时间和它是哪一个Generation有关,Young Generation的每次GC操作时间是最短的,Old Generation其次,Permanent Generation最长
- 无论哪一代,触发GC后,所有非垃圾回收线程暂停,GC结束后所有线程恢复执行
- 如果短时间内进行过多GC,多次暂停线程进行垃圾回收的累积时间就会增大.占用过多的帧间隔时间/16ms,导致CPU和GPU用于计算渲染的时间不足,导致卡顿/掉帧.
2.内存泄漏和内存溢出
内存泄漏就是无用对象占据的内存空间没有及时释放,导致内存空间浪费的情况.memory leak.
<br>
内存溢出是App为1个对象申请内存空间,内存空间不足的情况.out of memory.
<br>
内存泄漏数量足够大,就会引起内存溢出.或者说内存泄漏是内存溢出的原因之一.
3.Android性能优化典范-第2季
1.提升动画性能
- Bitmap的缩放,旋转,裁剪比较耗性能.例如在一个圆形的钟表图上,我们把时钟的指针抠出来当做单独的图片进行旋转会比旋转一张完整的圆形图性能好.
- 尽量减少每次重绘的元素可以极大提升性能.可以把复杂的View拆分会更小的View进行组合,在需要刷新界面时候仅对指定View进行重绘.
- 假如钟表界面上有很多组件,可以把这些组件做拆分,背景图片单独拎出来设置为一个独立的View,通过setLayerType()方法使得这个View强制用Hardware来进行渲染.至于界面上哪些元素需要做拆分,他们各自的更新频率是多少,需要有针对性的单独讨论
2.对象池
- 假如钟表界面上有很多组件,可以把这些组件做拆分,背景图片单独拎出来设置为一个独立的View,通过setLayerType()方法使得这个View强制用Hardware来进行渲染.至于界面上哪些元素需要做拆分,他们各自的更新频率是多少,需要有针对性的单独讨论
- 短时间内大量对象被创建然后很快被销毁,会多次触发Android虚拟机在Young generation进行GC,使用AS查看内存曲线,会看到内存曲线剧烈起伏,称为"内存抖动".
- GC会暂停其他线程,短时间多次GC/内存抖动会引起CPU和GPU在16ms内无法完成当前帧的渲染,引起界面卡顿.
- 避免内存抖动,可以使用对象池
-
实例
3.for index,for simple,iterator三种遍历性能比较
- 不要用for index去遍历链表,因为LinkedList在get任何一个位置的数据的时候,都会把前面的数据走一遍.应该使用Iterator去遍历
- get(0),直接拿到0位的Node0的地址,拿到Node0里面的数据
- get(1),直接拿到0位的Node0的地址,从0位的Node0中找到下一个1位的Node1的地址,找到Node1,拿到Node1里面的数据
- get(2),直接拿到0位的Node0的地址,从0位的Node0中找到下一个1位的Node1的地址,找到Node1,从1位的Node1中找到下一个2位的Node2的地址,找到Node2,拿到Node2里面的数据
- Vector和ArrayList,使用for index遍历效率较高
4.Merge:通过Merge减少1个View层级
- 可以将merge当做1个ViewGroup v,如果v的类型和v的父控件的类型一致,那么v其实没必要存在,因为白白增加了布局的深度.所以merge使用时必须保证merge中子控件所应该在的ViewGroup类型和merge所在的父控件类型一致.
- Merge的使用场景有2个:
- Activity的布局文件的根布局是FrameLayout,则将FrameLayout替换为merge
- 因为setContentView本质就是将布局文件inflate后加载到了id为android.id.content的FrameLayout上.
- merge作为根布局的布局文件通过include标签被引入其他布局文件中.这时候include所在的父控件,必须和merge所在的布局文件"原本根布局"一致.
- Activity的布局文件的根布局是FrameLayout,则将FrameLayout替换为merge
-
代码示例<br>
merge作为根布局的布局文件,用于Activity的setContentView:<br>
merge作为根布局的布局文件,被include标签引入其他布局文件中:
5.使用.9.png作为背景
- 典型场景是1个ImageView需要添加1个背景图作为边框.这样边框所在矩形的中间部分和实际显示的图片就好重叠发生Overdraw.
- 可以将背景图制作成.9.png.和前景图重叠部分设置为透明.Android的2D渲染器会优化.9.png的透明区域.
6.减少透明区域对性能的影响
- 不透明的View,显示它只需要渲染一次;如果View设置了alpha值,会至少需要渲染两次,性能不好
- 设置透明度setAlpha的时候,会把当前view绘制到offscreen buffer中,然后再显示出来.offscreen buffer是 一个临时缓冲区,把View放进来并做透明度的转化,然后显示到屏幕上,这个过程性能差,所以应该尽量避免这个过程
- 如何避免使用offscreen buffer
- 对于不存在过度绘制的View,如没有背景的TextView,就可以直接设置文字颜色;ImageView设置图片透明度setImageAlpha;自定义View设置绘制时的paint的透明度
- 如果是自定义View,确定不存在过度绘制,可以重写hasOverlappingRendering返回false即可.这样设置alpha时android会自动优化,避免使用offscreen buffer.
- 如果不是1,2两种情况,要设置View的透明度,则需要让GPU来渲染指定View,然后再设置透明度.
4.Android性能优化典范-第3季
1.避免使用枚举,用注解进行替代
- 枚举的问题
- 每个枚举值都是1个对象,相比较Integer和String常量,枚举的内存开销至少是其2倍.
- 过多枚举会增加dex大小及其中的方法数量,增加App占用的空间及引发65536几率
-
如何替代枚举:使用注解
- android.support.annotation中的@IntDef,@StringDef来包装Integer和String常量.
- 3个步骤
- 首先定义常量
- 然后自定义注解,设置取值范围就是刚刚定义的常量,并设置自定义注解的保留范围为源码时/SOURCE
- 位指定的属性及方法添加自定义注解.
-
代码实例
5.Android内存优化之OOM
如何避免OOM:
- 减小对象的内存占用
- 内存对象复用防止重建
- 避免内存泄漏
- 内存使用策略优化
1.减小对象的内存占用
- 避免使用枚举,用注解替代
- 减小创建的Bitmap的内存,使用合适的缩放比例及解码格式
- inSampleSize:缩放比例
- decode format:解码格式
- 现在很多图片资源的URL都可以添加图片尺寸作为参数.在通过网络获取图片时选择合适的尺寸,减小网络流量消耗,并减小生成的Bitmap的大小.
2.内存对象的重复利用
- 对象池技术:减少频繁创建和销毁对象带来的成本,实现对象的缓存和复用
- 尽量使用Android系统内置资源,可降低APK大小,在一定程度降低内存开销
- ConvertView的复用
- LRU的机制实现Bitmap的缓存(图片加载框架的必备机制)
- 在for循环中,用StringBuilder代替String实现字符串拼接
3.避免内存泄漏
- 在App中使用leakcanary检测内存泄漏:leakcanary
-
Activity的内存泄漏
-
Handler引起Activity内存泄漏
- 原因:Handler作为Activity的1个非静态内部类实例,持有Activity实例的引用.若Activity退出后Handler依然有待接收的Message,这时候发生GC,Message-Handler-Activity的引用链导致Activity无法被回收.
-
2种解决方法
- 在onDestroy调用Handler.removeCallbacksAndMessages(null)移除该Handler关联的所有Message及Runnable.再发生GC,Message已经不存在,就可以顺利的回收Handler及Activity
-
自定义静态内部类继承Handler,静态内部类实例不持有外部Activity的引用.在自定义Handler中定义外部Activity的弱引用,只有弱引用关联的外部Activity实例未被回收的情况下才继续执行handleMessage.自定义Handler持有外部Activity的弱引用,发生GC时不耽误Activity被回收.
- 在避免内存泄漏的前提下,如果要求Activity退出就不执行后续动作,用方法1.如果要求后续动作在GC发生前继续执行,使用方法2
-
- Context:尽量使用Application Context而不是Activity Context,避免不经意的内存泄漏
- 资源对象要及时关闭
4.内存使用策略优化
- 图片选择合适的文件夹进行存放
- hdpi/xhdpi/xxhdpi等等不同dpi的文件夹下的图片在不同的设备上会经过scale的处理。例如我们只在hdpi的目录下放置了一张100100的图片,那么根据换算关系,xxhdpi的手机去引用那张图片就会被拉伸到200200。需要注意到在这种情况下,内存占用是会显著提高的。对于不希望被拉伸的图片,需要放到assets或者nodpi的目录下
- 谨慎使用依赖注入框架.依赖注入框架会扫描代码,需要大量的内存空间映射代码.
- 混淆可以减少不必要的代码,类,方法等.降低映射代码所需的内存空间
- onLowMemory()与onTrimMemory():没想到应该怎么用
- onLowMemory
- 当所有的background应用都被kill掉的时候,forground应用会收到onLowMemory()的回调.在这种情况下,需要尽快释放当前应用的非必须的内存资源,从而确保系统能够继续稳定运行
- onTrimMemory(int level)
- 当系统内存达到某些条件的时候,所有正在运行的应用都会收到这个回调,同时在这个回调里面会传递以下的参数,代表不同的内存使用情况,收到onTrimMemory()回调的时候,需要根据传递的参数类型进行判断,合理的选择释放自身的一些内存占用,一方面可以提高系统的整体运行流畅度,另外也可以避免自己被系统判断为优先需要杀掉的应用
- onLowMemory
6.Android开发最佳实践
1.注意对隐式Intent的运行时检查保护
- 类似打开相机等隐式Intent,不一定能够在所有的Android设备上都正常运行.
- 例如系统相机应用被关闭或者不存在相机应用,或者某些权限被关闭都可能导致抛出ActivityNotFoundException的异常.
- 预防这个问题的最佳解决方案是在发出这个隐式Intent之前调用resolveActivity做检查
- 代码实例
2.Android 6.0的权限
3.MD新控件的使用:Toolbar替代ActionBar,AppBarLayout,Navigation Drawer, DrawerLayout, NavigationView等
7.Android性能优化典范-第4季
1.网络数据的缓存.okHttp,Picasso都支持网络缓存
okHttp Picasso
<br>
MVP架构实现的Github客户端(4-加入网络缓存)
2.代码混淆
2.1.AS中生成keystore.jks应用于APK打包
<br>
-
1:生成keystore.jks
<br> - 2:查看.jks文件的SHA1安全码
<br>
在AS的Terminal中输入:
<br>
keytool -list -v -keystore C:\Users\Administrator\Desktop\key.jks
<br>
keytool -list -v -keystore .jks文件详细路径
<br>
回车后,输入密钥库口令/就是.jks的密码,输入过程不可见,输入完毕回车即可!
<br>
2.2.proguard-rules关键字及部分通配符含义
<table align="center">
<tr>
<td>关键字</td>
<td>描述</td>
</tr>
<tr>
<td>keep</td>
<td>保留类和类中的成员,防止它们被混淆或移除</td>
</tr>
<tr>
<td>keepnames</td>
<td>保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除</td>
</tr>
<tr>
<td>keepclasseswithmembers</td>
<td>保留类和类中的成员,防止它们被混淆或移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆</td>
</tr>
<tr>
<td>keepclasseswithmembernames</td>
<td>保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆</td>
</tr>
<tr>
<td>keepclassmembers</td>
<td>只保留类中的成员,防止它们被混淆或移除</td>
</tr>
<tr>
<td>keepclassmembernames</td>
<td>只保留类中的成员,防止它们被混淆,但当成员没有被引用时会被移除</td>
</tr>
</table>
<br>
<table align="center">
<tr>
<td>通配符</td>
<td>描述</td>
</tr>
<tr>
<td>< field ></td>
<td>匹配类中的所有字段</td>
</tr>
<tr>
<td>< method ></td>
<td>匹配类中的所有方法</td>
</tr>
<tr>
<td>< init ></td>
<td>匹配类中的所有构造函数</td>
</tr>
<tr>
<td></td>
<td>
1.和字符串联合使用,代表任意长度的不包含包名分隔符(.)的字符串:<br>
a.b.c.MainActivity: a...MainActivity可以匹配;a.就匹配不上;<br>
2.*单独使用,就可以匹配所有东西
</td>
</tr>
<tr>
<td></td>
<td>
匹配任意长度字符串,包含包名分隔符(.)<br>
a.b.可以匹配a.b包下所有内容,包括子包
</td>
</tr>
<tr>
<td></td>
<td>
匹配任意参数类型.比如:<br>
void set(**)就能匹配任意传入的参数类型;<br>
get*()就能匹配任意返回值的类型
</td>
</tr>
<tr>
<td>…</td>
<td>
匹配任意长度的任意类型参数.比如:<br>
void test(…)就能匹配任意void test(String a)或者是void test(int a, String b)
</td>
</tr>
</table>
<br>
- keep 完整类名{*;}, 可以对指定类进行完全保留,不混淆类名,变量名,方法名.<br>
- 在App中,我们会定义很多实体bean.往往涉及到bean实例和json字符串间互相转换.部分json库会通过反射调用bean的set和get方法.因而实体bean的set,get方法不能被混淆,或者说我们自己写的方法,如果会被第三方库或其他地方通过反射调用,则指定方法要keep避免混淆.
- 我们自己写的使用了反射功能的类,必须keep
- 如果我们要保留继承了指定类的子类,或者实现了指定接口的类
2.3.proguard-rules.pro通用模板
2.4.混淆jar包
<br>
郭霖大神博客有介绍,自己没试过
2.5.几条实用的Proguard rules
<br>
在上面提供的通用模板上继续添加下面几行:
- repackageclasses:除了keep的类,会把我们自己写的所有类以及所使用到的各种第三方库代码统统移动到我们指定的单个包下.
- 比如一些比较敏感的被keep的类在包a.b.min下,我们可以使用 -repackageclasses a.b.min,这样就有成千上万的被混淆的类和未被混淆的敏感的类在a.b.min下面,正常人根本就找不到关键类.尤其是keep的类也只是保留关键方法,名字也被混淆过.
- -obfuscationdictionary,-classobfuscationdictionary和-packageobfuscationdictionary分别指定变量/方法名,类名,包名混淆后的字符串集.
- 默认我们的代码命名会被混淆成字母组合,使用这些配置可以用乱码或中文内容进行命名.中文命名可以破坏部分反编译软件的正常工作,乱码则极大加大了查看代码的难度.
- dict.txt:需要放到和app模块的proguard-rules.pro同级目录.dict.txt具体内容可以自己写,参考开源项目:一种生成阅读极其困难的proguard字典的算法
- -assumenosideeffects class android.util.Log是在编译成 APK 之前把日志代码全部删掉.
- Androidstudio 混淆去掉日志 assumenosideeffects 不起作用
- 因为默认情况下,使用的是proguard-android.txt的混淆规则.proguard-android.txt中包含-dontoptimize.-dontoptimize导致日志语句不会被优化掉.所以我们提供的模板不包含-dontoptimize这一句.
- Androidstudio 混淆去掉日志 assumenosideeffects 不起作用
2.6.字符串硬编码
- 对于反编译者来说,最简单的入手点就是字符串搜索.硬编码留在代码里的字符串值都会在反编译过程中被原样恢复,不要使用硬编码.
- 如果一定要使用硬编码
- 新建1个存储硬编码的常量类,静态存放字符串常量,即使找到了常量类,反编译者很难搜索到哪里用了这些字符串.
- 常量类中的静态常量字符串,用名称作为真正内容,而值用难以理解的编码表示.
2.7.res资源混淆及多渠道打包
<br>
简单讲,使用腾讯的2个gradle插件来实现res资源混淆及多渠道打包.
<br>
res资源混淆:AndResGuard
<br>
多渠道打包:VasDolly
<br>
多渠道打包原理+VasDolly和其他多渠道打包方案对比
<br><br>
具体流程:
<br>
AndResGuard使用了chaychan的方法,单独创建gradle文件
<br>
-
项目根目录下build.gradle中,添加插件的依赖,具体如下
-
在app目录下单独创建gradle文件and_res_guard.gradle.内容如下
-
模块app下的build.gradle文件添加依赖,具体如下
-
首先使用AndResGuard实现资源混淆,再使用VasDolly实现多渠道打包
-
在Gradle界面中,找到app模块下andresguard的task.
- 如果想打debug包,则执行resguardDebug指令;
- 如果想打release包,则执行resguardRelease指令.
- 此处我们双击执行resguardRelease指令,在app目录下的/build/output/apk/release/AndResGuard_{apk_name}/ 文件夹中找到混淆后的Apk,其中app-release_aligned_signed.apk为进行混淆并签名过的apk.
- 我们查看app-release_aligned_signed.apk,res文件夹更名为r,里面的目录名称以及xml文件已经被混淆.
- 将app-release_aligned_signed.apk放到app模块下,在Gradle界面中,找到app模块下channel的task,执行reBuildChannel指令.
- 双击执行reBuildChannel指令,几秒钟就生成了20个通过app-release_aligned_signed.apk的多渠道apk.
- 通过helper类库中的ChannelReaderUtil类读取渠道信息
- 双击执行reBuildChannel指令,几秒钟就生成了20个通过app-release_aligned_signed.apk的多渠道apk.
-
3.APK瘦身
4.更高效的数据序列化:只是看看从没用过,Protocal Buffers,Nano-Proto-Buffers,FlatBuffers
5.数据呈现的顺序以及结构会对序列化之后的空间产生不小的影响
-
gzip
- gzip概念:HTTP协议上的GZIP编码是一种用来改进WEB应用程序性能的技术
- 一般对纯文本内容可压缩到原大小的40%
- 减少文件大小有两个明显的好处,一是可以减少存储空间,二是通过网络传输文件时,可以减少传输的时间
-
okHttp对gzip的支持
- okHttp支持gzip自动解压缩,不需要设置Accept-Encoding为gzip
- 开发者手动设置Accept-Encoding,okHttp不负责解压缩
- 开发者没有设置Accept-Encoding时,则自动添加Accept-Encoding: gzip,自动添加的request,response支持自动解压
- 自动解压时移除Content-Length,所以上层Java代码想要contentLength时为-1
- 自动解压时移除 Content-Encoding
- 自动解压时,如果是分块传输编码,Transfer-Encoding: chunked不受影响
-
我们在向服务器提交大量数据的时候,希望对post的数据进行gzip压缩,需要使用自定义拦截器
- okHttp支持gzip自动解压缩,不需要设置Accept-Encoding为gzip
- gzip概念:HTTP协议上的GZIP编码是一种用来改进WEB应用程序性能的技术
-
改变数据结构,就是将原始集合按照集合中对象的属性进行拆分,变成多个属性集合的形式.
- 改变数据结构后JSON字符串长度有明显降低
- 使用GZIP对JSON字符串进行压缩,在原始集合中元素数据重复率逐渐变大的情况下,GZIP压缩后的原始JSON字符串长度/GZIP压缩后的改变数据结构的JSON字符串会明显大于1.
-
代码及测试结果如下,结果仅供参考(ZipUtils是网上荡的,且没有使用网上用过的Base64Decoder,Base64Encoder)
8.Android性能优化典范-第5季
多线程大部分内容源自凯哥的课程,个人觉得比优化典范写得清晰得多
1.线程
- 线程就是代码线性执行,执行完毕就结束的一条线.UI线程不会结束是因为其初始化完毕后会执行死循环,所以永远不会执行完毕.
- 如何简单创建新线程:
- 两种方式创建新线程性能无差别,使用Runnable实例适用于希望Runnable复用的情形
- 常用的创建线程池2种方式
- Executors.newCachedThreadPool():一般情况下使用newCachedThreadPool即可.
- Executors.newFixedThreadPool(int number):短时批量处理/比如要并行处理多张图片,可以直接创建包含图片精确数量的线程的线程池并行处理.
- 《阿里巴巴Java开发手册》规定:
- 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式.这样
的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 - 看Android中Executors源码.Executors.newCachedThreadPool/newScheduledThreadPool允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM.而newFixedThreadPool,newSingleThreadExecutor不会存在这种风险.
- 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式.这样
- [如何正确创建ThreadPoolExecutor:有点麻烦,晚点详述]()
- ExecutorService的shutdown和shutdownNow
- shutdown:在调用shutdown之前ExecutorService中已经启动的线程,在调用shutdown后,线程如果执行未结束会继续执行完毕并结束,但不会再启动新的线程执行新任务.
- shutdownNow:首先停止启动新的线程执行新任务;并尝试结束所有正在执行的线程,正在执行的线程可能被终止也可能会继续执行完成.
-
如何正确创建ThreadPoolExecutor<br>
3.1:ThreadPoolExecutor构造参数- int corePoolSize:该线程池中核心线程最大数量.默认情况下,即使核心线程处于空闲状态也不会被销毁.除非通过allowCoreThreadTimeOut(true),则核心线程在空闲时间达到keepAliveTime时会被销毁<br>
- int maximumPoolSize:该线程池中线程最大数量<br>
- long keepAliveTime:该线程池中非核心线程被销毁前最大空闲时间,时间单位由unit决定.默认情况下核心线程即使空闲也不会被销毁,在调用allowCoreThreadTimeOut(true)后,该销毁时间设置也适用于核心线程<br>
- TimeUnit unit:keepAliveTime/被销毁前最大空闲时间的单位<br>
- BlockingQueue<Runnable> workQueue:该线程池中的任务队列.维护着等待被执行的Runnable对象.BlockingQueue有几种类型,下面会详述<br>
- ThreadFactory threadFactory:创建新线程的工厂.一般情况使用Executors.defaultThreadFactory()即可.当然也可以自定义.<br>
- RejectedExecutionHandler handler:拒绝策略.当需要创建的线程数量达到maximumPoolSize并且等待执行的Runnable数量超过了任务队列的容量,该如何处理.<br>
3.2:当1个任务被放进线程池,ThreadPoolExecutor具体执行策略如下:<br>
- 如果线程数量没有达到corePoolSize,有核心线程空闲则核心线程直接执行,没有空闲则直接新建核心线程执行任务;
- 如果线程数量已经达到corePoolSize,且核心线程无空闲,则将任务添加到等待队列;
- 如果等待队列已满,则新建非核心线程执行该任务;
- 如果等待队列已满且总线程数量已达到maximumPoolSize,则会交由RejectedExecutionHandler handler处理.<br>
3.3:阻塞队列/BlockingQueue<Runnable> workQueue<br>
- BlockingQueue有如下几种:SynchronousQueue/LinkedBlockingQueue/LinkedTransferQueue/ArrayBlockingQueue/PriorityBlockingQueue/DelayQueue.
- SynchronousQueue:SynchronousQueue的容量是0,不存储任何Runnable实例.新任务到来会直接尝试交给线程执行,如所有线程都在忙就创建新线程执行该任务.
- LinkedBlockingQueue:默认情况下没有容量限制的队列.
- ArrayBlockingQueue:一个有容量限制的队列.
- DelayQueue:一个没有容量限制的队列.队列中的元素必须实现了Delayed接口.元素在队列中的排序按照当前时间的延迟值,延迟最小/最早要被执行的任务排在队列头部,依次排序.延迟时间到达后执行指定任务.
- PriorityBlockingQueue:一个没有容量限制的队列.队列中元素必须实现了Comparable接口.队列中元素排序依赖元素的自然排序/compareTo的比较结果.
- 各种BlockingQueue的问题<br>
1.SynchronousQueue缺点:因为不具备存储元素的能力,因而当任务很频繁时候,为了防止线程数量超标,我们往往设置maximumPoolSize是Integer.MAX_VALUE,创建过多线程会导致OOM.《阿里巴巴Java开发手册》中强调不能使用Executors直接创建线程池,就是对应Android源码中newCachedThreadPool和newScheduledThreadPool,本质上就是创建了maximumPoolSize为Integer.MAX_VALUE的ThreadPoolExecutor.<br>
2.LinkedBlockingQueue因为没有容量限制,所以我们使用LinkedBlockingQueue创建ThreadPoolExecutor,设置maximumPoolSize是无意义的,如果线程数量已经达到corePoolSize,且核心线程都在忙,那么新来的任务会一直被添加到队列中.只要核心线程无空闲则一直得不到被执行机会.<br>
3.DelayQueue和PriorityBlockingQueue也具有同样的问题.所以corePoolSize必须设置合理,否则会导致超出核心线程数量的任务一直得不到机会被执行.这两类队列分别适用于定时及优先级明确的任务.<br>
3.4:RejectedExecutionHandler handler/拒绝策略有4种<br>
1.hreadPoolExecutor.AbortPolicy:丢弃任务,并抛出RejectedExecutionException异常.ThreadPoolExecutor默认就是使用AbortPolicy.<br>
2.ThreadPoolExecutor.DiscardPolicy:丢弃任务,但不会抛出异常.<br>
3.ThreadPoolExecutor.DiscardOldestPolicy:丢弃排在队列头部的任务,不抛出异常,并尝试重新执行任务.<br>
4.ThreadPoolExecutor.CallerRunsPolicy:丢弃任务,但不抛出异常,并将该任务交给调用此ThreadPoolExecutor的线程执行. - synchronized 的本质
- 保证synchronized方法或者代码块内部资源/数据的互斥访问
- 即同一时间,由同一个Monitor监视的代码,最多只有1个线程在访问
- 保证线程之间对监视资源的数据同步.
- 任何线程在获取Monitor后,会第一时间将共享内存中的数据复制到自己的缓存中;
- 任何线程在释放Monitor后,会第一时间将缓存中的数据复制到共享内存中
- 保证synchronized方法或者代码块内部资源/数据的互斥访问
-
volatile
- 保证被volatile修饰的成员的操作具有原子性和同步性.相当于简化版的synchronized
- 原子性就是线程间互斥访问
- 同步性就是线程之间对监视资源的数据同步
-
volatile生效范围:基本类型的直接复制赋值 + 引用类型的直接赋值
- volatile型变量自增操作的隐患
- volatile类型变量每次在读取的时候,会越过线程的工作内存,直接从主存中读取,也就不会产生脏读
- ++自增操作,在Java对应的汇编指令有三条
- 从主存读取变量值到cpu寄存器
- 寄存器里的值+1
- 寄存器的值写回主存
- 如果N个线程同时执行到了第1步,那么最终变量会损失(N-1).第二步第三步只有一个线程是执行成功.
- 对变量的写操作不依赖于当前值,才能用volatile修饰.
- volatile型变量自增操作的隐患
- 保证被volatile修饰的成员的操作具有原子性和同步性.相当于简化版的synchronized
- 针对num++这类复合类的操作,可以使用java并发包中的原子操作类原子操作类:AtomicInteger AtomicBoolean等来保证其原子性.
2.线程间交互
-
一个线程终结另一个线程
-
Thread.stop不要用:
- 因为线程在运行过程中随时有可能会被暂停切换到其他线程,stop的效果相当于切换到其他线程继续执行且以后再也不会切换回来.我们执行A.stop的时候,完全无法预知A的run方法已经执行了多少,执行百分比完全不可控.
private static void t2(){
Thread t = new Thread(){br/>@Override
public void run() {
for(int i=0;i<1000000;i++){
System.out.println(""+i);
}
}
};
t.start();
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.stop();
}
-
-
Thread.interrupt:仅仅设置当前线程为被中断状态.在运行的线程依然会继续运行.
- Thread.isInterrupted:获取当前线程是否被中断
- Thread.interrupted():如果线程A调用了Thread.interrupted()
- 如果A之前已经被中断,调用Thread.interrupted()返回false,A已经不是被中断状态
- 如果A之前不是被中断状态,调用Thread.interrupted()返回true,A变成被中断状态.
-
单纯调用A.interrupt是无效果的,interrupt需要和isInterrupted联合使用
- 用于我们希望线程处于被中断状态时结束运行的场景.
- interrupt和stop比较的优点:stop后,线程直接结束,我们完全无法控制当前执行到哪里;<br>
interrupt后线程默认会继续执行,我们通过isInterrupted来获取被中断状态,只有被中断且满足我们指定条件才return,可以精确控制线程的执行百分比.
......
799999
800000
Process finished with exit code 0 -
InterruptedException:
- 如果线程A在sleep过程中被其他线程调用A.interrupt(),会触发InterruptedException.
- 如果调用A.interrupt()时候,A并不在sleep状态,后面再调用A.sleep,也会立即抛出InterruptedException.
老子被叫醒了:睡了493ms
Process finished with exit code 0
-
线程等待:wait,notifyAll,notify
- wait,notifyAll,notify是属于Object的方法.用于线程等待的场景,需用Monitor进行调用
- wait:
- 当1个线程A持有Monitor M.
- 此时调用M.wait,A会释放M并处于等待状态.并记录A在当前代码执行的位置Position.
- notify:
- 当调用M.notify(),就会唤醒1个因为调用M.wait()而处于等待状态的线程
- 如果有A,B,C--多个线程都是因为调用M.wait()而处于等待状态,不一定哪个会被唤醒并尝试获取M
- notifyAll:
- 当调用M.notifyAll(),所有因为调用M.wait()而处于等待状态的线程都被唤醒,一起竞争尝试获取M
- 调用notify/notifyAll被唤醒并获取到M的线程A,会接着之前的代码执行位置Position继续执行下去
线程:Thread-2 尝试printStr时间:1539247468146
线程:Thread-1 尝试printStr时间:1539247468944
setStr时间:1539247469944
线程:Thread-1 printStr时间:1539247469944
str:老子设置一下
线程:Thread-2 printStr时间:1539247469944
str:老子设置一下
3.Executor、 AsyncTask、 HandlerThead、 IntentService 如何选择
- HandlerThead就不要用,HandlerThead设计目的就是为了主界面死循环刷新界面,无其他应用场景.
- 能用线程池就用线程池,因为最简单.
- 涉及后台线程推送任务到UI线程,可以使用Handler或AsyncTask
- Service:就是为了做后台任务,不要UI界面,需要持续存活.有复杂的需要长期存活/等待的场景使用Service.
- IntentService:属于Service.当我们需要使用Service,且需要后台代码执行完毕后该Service自动被销毁,使用IntentService.
4.AsyncTask的内存泄漏
- GC Roots:由堆外指向堆内的引用,包括:
- Java方法栈帧中的局部变量
- 已加载类的静态变量
- native代码的引用
- 运行中的Java线程
- AsyncTask内存泄漏本质:正在运行的线程/AsyncTask 在虚拟机中属于GC ROOTS,AsyncTask持有外部Activity的引用.被GC ROOTS引用的对象不能被回收.
- 所以AsyncTask和其他线程工具一样,只要是使用线程,都有可能发生内存泄漏,都要及时关闭,AsyncTask并不比其他工具更差.
5.RxJava.
讲的太多了这里推荐1个专题RxJava2.x<br>
下面记录一下自己不太熟的几点<br>
- RxJava整体结构:
- 链的最上游:生产者Observable
- 链的最下游:观察者Observer
- 链的中间多个节点:双重角色.即是上一节点的观察者Observer,也是下一节点的生产者Observable.
- Scheduler切换线程的原理:源码跟踪下去,实质是通过Excutor实现了线程切换.
6.Android M对Profile GPU Rendering工具的更新
- Swap Buffers:CPU等待GPU处理的时间
- Command Issur:OpenGL渲染Display List所需要的时间
- Sync&Upload:通常表示的是准备当前界面上有待绘制的图片所耗费的时间,为了减少该段区域的执行时间,我们可以减少屏幕上的图片数量或者是缩小图片本身的大小
- Draw:测量绘制Display List的时间
- Measure & Layout:这里表示的是布局的onMeasure与onLayout所花费的时间.一旦时间过长,就需要仔细检查自己的布局是不是存在严重的性能问题
- Animation:表示的是计算执行动画所需要花费的时间.包含的动画有ObjectAnimator,ViewPropertyAnimator,Transition等等.一旦这里的执行时间过长,就需要检查是不是使用了非官方的动画工具或者是检查动画执行的过程中是不是触发了读写操作等等
- Input Handling:表示的是系统处理输入事件所耗费的时间,粗略等于对于的事件处理方法所执行的时间.一旦执行时间过长,意味着在处理用户的输入事件的地方执行了复杂的操作
- Misc/Vsync Delay:如果稍加注意,我们可以在开发应用的Log日志里面看到这样一行提示:I/Choreographer(691): Skipped XXX frames! The application may be doing too much work on its main thread。这意味着我们在主线程执行了太多的任务,导致UI渲染跟不上vSync的信号而出现掉帧的情况
9.Android性能优化典范-第6季
1.启动闪屏
- 当点击桌面图标启动APP的时候,App会出现短暂的白屏,一直到第一个Activity的页面的渲染加载完毕
-
为了消除白屏,我们可以为App入口Activity单独设置theme.
- 在单独设置的theme中设置android:background属性为App的品牌宣传图片背景.
-
在代码执行到入口Activity的onCreate的时候设置为程序正常的主题.
AndroidManifest.xmlandroid:icon="@mipmap/ic_launcher"<br "="" rel="nofollow">br/><application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"br/>android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"br/>android:theme="@style/AppTheme">
<activity android:name=".MainActivity"br/>android:theme="@style/ThemeSplash">//为入口Activity单独指定theme
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</manifest>public class MainActivity extends AppCompatActivity {br/>@Override
protected void onCreate(Bundle savedInstanceState) {
//在代码执行到入口Activity时候设置入口Activity为默认主题
setTheme(R.style.AppTheme);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv_all = findViewById(R.id.tv_all);
tv_local = findViewById(R.id.tv_local);
//注册全局广播
registerReceiver(globalReceiver,new IntentFilter("global"));
//注册本地广播
LocalBroadcastManager.getInstance(this).registerReceiver(localBroadReceiver,new IntentFilter("localBroadCast"));
}
}
- 如果是只有1张图片,放在mipmap-nodpi,或mipmap-xxxhdpi下
- 所有的大背景图片,统一放在mipmap-nodpi目录,用一套1080P素材可以解决大部分手机适配问题,不用每个资源目录下放一套素材
- 经过试验,不论ImageView宽高是否是wrap_content,只要图片所在文件夹和当前设备分辨率不匹配,都会涉及到放大或压缩,占用的内存都会相应的变化.尤其对于大图,放在低分辨率文件夹下直接OOM.
<br>具体原因:<br>
郭霖:Android drawable微技巧,你所不知道的drawable的那些细节当我们使用资源id来去引用一张图片时,Android会使用一些规则来去帮我们匹配最适合的图片。什么叫最适合的图片?比如我的手机屏幕密度是xxhdpi,那么drawable-xxhdpi文件夹下的图片就是最适合的图片。因此,当我引用android_logo这张图时,如果drawable-xxhdpi文件夹下有这张图就会优先被使用,在这种情况下,图片是不会被缩放的。但是,如果drawable-xxhdpi文件夹下没有这张图时, 系统就会自动去其它文件夹下找这张图了,优先会去更高密度的文件夹下找这张图片,我们当前的场景就是drawable-xxxhdpi文件夹,然后发现这里也没有android_logo这张图,接下来会尝试再找更高密度的文件夹,发现没有更高密度的了,这个时候会去drawable-nodpi文件夹找这张图,发现也没有,那么就会去更低密度的文件夹下面找,依次是drawable-xhdpi -> drawable-hdpi -> drawable-mdpi -> drawable-ldpi。
总体匹配规则就是这样,那么比如说现在终于在drawable-mdpi文件夹下面找到android_logo这张图了,但是系统会认为你这张图是专门为低密度的设备所设计的,如果直接将这张图在当前的高密度设备上使用就有可能会出现像素过低的情况,于是系统自动帮我们做了这样一个放大操作。
那么同样的道理,如果系统是在drawable-xxxhdpi文件夹下面找到这张图的话,它会认为这张图是为更高密度的设备所设计的,如果直接将这张图在当前设备上使用就有可能会出现像素过高的情况,于是会自动帮我们做一个缩小的操作
3.尽量复用已经存在的图片.<br>
比如一张图片O已经存在,如果有View的背景就是O旋转过后的样子,可以直接用O创建RotateDrawable.然后将设置给View使用.<br>
注意:RotateDrawable已经重写了其onLevelChange方法,所以一定要设置level才会生效<br>
实例:
4.开启混淆和资源压缩:在app模块下的的build.gradle中
5.对于简单/规则纹理的图片,使用VectorDrawable来替代多个分辨率图片.VectorDrawable 有很多注意事项,后面单独一篇文章总结
10.网络优化
网络优化主要有几个方面:降低网络请求数量,降低单次请求响应的数据量,在弱网环境下将非必要网络请求延缓至网络环境好的时候.
1.降低网络请求数量:获取同样的数据,多次网络请求会增加电量消耗,且多次请求总体上将消耗服务端更多的时间及资源<br>
- 接口Api设计要合理.可以将多个接口合并,多次请求显示1个界面,改造后1个接口即可提供完整数据.
- 根据具体场景实时性需求,在App中加入网络缓存,在实时性有效区间避免重复请求:主要包括网络框架和图片加载框架的缓存.
2.降低单次请求的数据量
- 网络接口Api在设计时候,去除多余的请求参数及响应数据.
- 网络请求及响应数据的传输开启GZIP压缩,降低传输数据量.
- okHttp对gzip的支持前面已记录
- Protocal Buffers,Nano-Proto-Buffers,FlatBuffers代替GSON执行序列化.
- Protocal Buffers网上有使用的方法,相对GSON有点繁琐.如果对网络传输量很敏感,可以考虑使用.其他几种方案的文章不多.
- 网络请求图片,添加图片宽高参数,避免下载过大图片增加流量消耗.
3.弱网环境优化这块没有经验,直接看anly_jun的文章
App优化之网络优化<br>
文章中提到:用户点赞操作, 可以直接给出界面的点赞成功的反馈, 使用JobScheduler在网络情况较好的时候打包请求.
11.电量优化
12.JobScheduler,AlarmManager和WakeLock
JobScheduler在网络优化中出现过,WakeLock涉及电量优化,AlarmManager和WakeLock有相似,但侧重点不同.
- WakeLock:比如一段关键逻辑T已经在执行,执行未完成Android系统就进入休眠,会导致T执行中断.WakeLock目的就在于阻止Android系统进入休眠状态,保证T得以继续执行.
- 休眠过程中自定义的Timer、Handler、Thread、Service等都会暂停
- AlarmManager:Android系统自带的定时器,可以将处于休眠状态的Android系统唤醒
- 保证Android系统在休眠状态下被及时唤醒,执行 定时/延时/轮询任务
- JobScheduler:JobScheduler目的在于将当下不紧急的任务延迟到后面更合适的某个时间来执行.我们可以控制这些任务在什么条件下被执行.
- JobScheduler可以节约Android设备当下网络,电量,CPU等资源.在指定资源充裕情况下再执行"不紧要"的任务.
JobScheduler:<br>
Android Jobscheduler使用<br>
Android开发笔记(一百四十三)任务调度JobScheduler<br>
WakeLock:<br>
Android WakeLock详解<br>
Android PowerManager.WakeLock使用小结<br>
Android的PowerManager和PowerManager.WakeLock用法简析<br>
AlarmManager和WakeLock使用:<br>
后台任务 - 保持设备唤醒状态
13.性能检测工具
1.Android Studio 3.2之后,Android Device Monitor已经被移除.Android Device Monitor原先包含的工具由新的方案替代.Android Device Monitor
- DDMS:由Android Profiler代替.可以进行CPU,内存,网络分析.
- TraceView:可以通过Debug类在代码中调用Debug.startMethodTracing(String tracePath)和Debug.stopMethodTracing()来记录两者之间所有线程及线程中方法的耗时,生成.trace文件.通过abd命令可以将trace文件导出到电脑,通过CPU profiler分析.
- Systrace:可以通过命令行生成html文件,通过Chrome浏览器进行分析.
- 生成html文件已实现.但文件怎么分析暂未掌握,看了网上一些文章说实话还是没搞懂
- Hierarchy Viewer:由Layout Inspector代替.但当前版本的Layout Inspector不能查看每个View具体的onMeasure,onLayout,onDraw耗时,功能是不足的.我们可使用系统提供的Window.OnFrameMetricsAvailableListener来计算指定View的onLayout及onDraw耗时.
- Network Traffic tool:由Network Profiler代替.
2.其中Android Profiler如何使用,直接看官网即可.Profile your app performance.
3.TraceView
- TraceView可以直接通过CPU profiler中点击Record按钮后,任意时间后点击Stop按钮.即可生成trace文件.并可将.trace文件导出.
- TraceView也可以通过Debug类在代码中精确控制要统计哪个区间代码/线程的CPU耗时.
<br>这种用法是 anly_jun大神文章里学到的代码执行完毕,会在Android设备中生成JetApp.trace文件.通过Device File Explorer,找到sdcard/Android/data/app包名/files/JetApp.trace<br>
在JetApp.trace上点击右键->Copy Path,将trace文件路径复制下来.<br>
Windows下cmd打开命令行,执行 adb pull 路径,即可trace文件导出到电脑. - trace文件分析很简单.我们可以看到每个线程及线程中每个方法调用消耗的时间.
4.Layout Inspector很简单,在App运行后,点击Tools->Layout Inspector即可.
下面只看Window.OnFrameMetricsAvailableListener怎么用.
从Android 7.0 (API level 24)开始,Android引入Window.OnFrameMetricsAvailableList接口用于提供每一帧绘制各阶段的耗时,数据源与GPU Profile相同.
-
在Activity中使用OnFrameMetricsAvailableListener:
- 通过调用this.getWindow().addOnFrameMetricsAvailableListener(@NonNull OnFrameMetricsAvailableListener listener,Handler handler)来添加监听.
- 通过调用this.getWindow().removeOnFrameMetricsAvailableListener(OnFrameMetricsAvailableListener listener)取消监听.
-
解析ConstraintLayout的性能优势中引用了Google使用OnFrameMetricsAvailableListener的例子android-constraint-layout-performance.其中Activity是Kotlin写的,尝试将代码转为java,解决掉报错后运行.
- 在Application中使用OnFrameMetricsAvailableListener,则可以统一设置,不需要每个Activity单独设置,推荐使用开源项目ActivityFrameMetrics
- 在Application的onCreate中设置单帧渲染总时间超过W毫秒和E毫秒,会在Logcat中打印警告和错误的Log信息.
- Application设置完成运行App,出现单帧渲染总耗时超过指定时间,即可看到Logcat中的信息.
5.Systrace:通过命令行生成html文件,通过Chrome浏览器进行分析
- 首先电脑要安装python,这里有几个坑:<br>
- Python要安装2.7x版本,不能安装最新的3.x.
- 比如自己电脑中systrace文件夹路径是:C:\Users\你的用户名\AppData\Local\Android\Sdk\platform-tools\systrace,如果我们安装的是3.x版本,在这个路径下执行python systrace.py *** 命令会报错,提示你应该安装2.7
- Python安装时候,要记得勾选"Add python.exe to Path".
- 这时候直接执行python systrace.py ***命令还是会报错:ImportError: No module named win32com
- Python要安装2.7x版本,不能安装最新的3.x.
- 生成html及如何分析