运行快,运行稳定、体积小,电量/流量节省,基本上可以从这四个方面确定一个APP是否是性能良好的APP,这四个方面对应于APP卡顿、内存泄漏/崩溃,代码质量和逻辑,安装包体积四个层面
参考
推荐
安卓性能优化在应用层的话,大概就下面这些优化点,当然平时代码里还有一些小优化,那就另说。实践并熟悉以下优化,可以算是入门了吧。
一、布局优化
屏幕上的某个像素在同一帧的时间内被绘制了多次,在多层的UI结构里面,如果不可见的UI也在做绘制,这就会导致某些像素区域被绘制了 多次。这就浪费大量的CPU以及GPU资源 。
Android虚拟机内存本来就不够用,如果占用CPU计算实际那和GPU渲染的时间,显然不行。通过AS的xml编辑器底部可以看到所在标签层级颜色变化,尽量不要超过三层
- 如果父控件有颜色,子空间也是同样背景颜色,就不必为子控件添加背景颜色
- 如果每个控件颜色不一样,可以完全覆盖父控件,就不需要为父控件添加颜色
- 尽量减少不必要的嵌套
- 能用LinearLayout和FrameLayout,就不要用RelativeLayout,因为RelativeLayout控件相对比较复杂,测量绘制也相对耗时,因为RelativeLayout会测量每个子节点两次
- 针对嵌套布局
- include 可以提高布局的复用性,不过这个确实没有减少布局嵌套
- merge 该布局取决于父控件是哪个布局,使用merge相当于减少了自身的一层布局,配合include使用可达到复用和减少嵌套作用
- ViewStub 按需加载,需要使用它的时候再来加载到内存,对于一些只用一次或者可能用不到的功能,这个是很合适的(通过setVisibility(View.VISIBLE) 或者 inflate() 方法)
- 当然有时候布局已经精简了,但是实现不了复杂界面怎么办?可以使用ConstraintLayout布局
- 使用clipRect() 指定位置 ,减少自定义View的过度绘制,防止绘制了一张图片后,第二张图片绘制后覆盖在第一张上等等问题。
二、绘制优化
布局优化了,但是布局加载必定会影响到绘制,并且布局优化的主要目的还是想促使绘制变快,也就是渲染到屏幕上。
平时界面卡顿往往是Android的渲染机制造成的
-
Android系统每隔16ms发出VSYNC信号,触发UI进行渲染,但是渲染未必成功,如果失败了可能就会延误时间或者跳过去,给人的视觉效果就是卡了一会。
View的绘制频率保证在60fps是最佳的,这就要求每帧绘制时间不超过16ms (16ms = 1000/60),虽然这个过程很难保证16ms,不过尽量降低onDraw方法中的复杂度总是有点效果的,能优化一点是一点
正常情况,每隔16ms绘制一次,很整齐流畅
异常绘制,有个绘制耗时,占用了很长时间导致后续绘制延迟了
-
从以上可以得出两点
- onDraw方法不要做耗时任务,也不要做过多的循环操作,特别是嵌套循环,每次循环耗时小,但是大量耗时还是会占用CPU的时间的
- onDraw中不要创建新的局部对象,万一onDraw方法频繁被调用,就会产生大量的临时对象,占用内存,导致频繁GC
三、内存优化
内存泄漏(Memory Leak)指的是那些程序不再使用的对象无法被GC识别,这样就导致对象一直留在内存当中,占用了内存,它是一个慢慢积累的过程,内存不够用的时候程序就奔溃了。(此处建议了解下JVM内存分配、GC与垃圾收集器)
GC触发时所有的线程都是暂停状态的,需要处理的对象数量越多越耗时,然后就造成卡顿
- 集合类泄漏
- 集合类添加元素后,仍然引用着集合元素对象,导致该集合中的元素无法被回收
- 单例 / 静态 变量造成的内存泄漏
- 例如传递了Activity的Context给了单例,而单例的生命周期和应用程序一样,应该使用getApplicationContext
- 还有一个常用的就是Toast
- 匿名内部类 / 非静态 内部类
- 非静态内部类会自动持有外部类强引用,生命周期未知
- 弱引用配合Activity和静态Handler使用
- 资源未关闭造成的内存泄漏
- 网络、文件等流忘记关闭
- 单例拿到Activity的引用没有释放或者说是广播等未解注册
- 可以用LeakCanary检测、Memory Monitor或者Android Lint检测或者Memory Profiler(Android Profiler的一个组件)
四、启动优化
App启动分为冷启动、热启动、温启动三类
-
冷启动
- 应用从头开始,在此之前没有创建应用程序,属于首次启动应用或者是系统终止应用后首次启动
- 过程:1.加载并启动应用程序 2. 启动后立即显示应用程序的空白启动窗口 3.创建应用程序进程
- 当Application启动时,空白的启动窗口将保留在屏幕上,直到系统首次完成绘制应用程序。此时,系统进程会交换应用程序的启动窗口,允许用户开始与应用程序进行交互,这就是为什么我们的程序启动时会先出现一段时间的黑屏(白屏)
- 应用从程序进程完成第一次绘制后,系统进程会交换当前显示的背景窗口,将其替换为主活动。
-
热启动
- 直接把Activity带到前台。如果应用程序的Activity仍然驻留在内存中,那么应用程序可以避免重复对象初始化,布局加载和渲染
- 和冷启动有相同的屏幕行为,系统进程显示空白屏幕,直到应用程序完成呈现活动
-
温启动
- 用户退出应用,但随后重新启动它,该过程可能已继续运行,但应用程序必须通过调用从头开始重新创建Activity的onCreate()
- 系统将应用程序从内存中逐出,然后用户重新启动它。需要重新启动进程和活动,但是在调用onCreate时候可以从Bundle(savedInstanceState)获取数据
-
AI和启动方式的联系
- AI进程唤醒功能,原理就是学习用户的使用习惯,提前将APP进程创建好,当用户打开APP时就不会出现冷启动。
-
谷歌官方建议
- 利用提前展示出来的Window,快速展示出来一个界面,给用户快速反馈的体验
- 避免在启动时做密集沉重的初始化
- 避免IO操作、反序列化、网络操作、布局嵌套等
-
对启动时间进行检测
-
机械手和高速相机测试,这种方式最直观,可是成本很高
-
通过shell命令
adb shell am start -W[packageName]/[packageName.MainActivity]
成功后由三个测量到的时间
ThisTime : 一般和totalTime时间一样
TotalTime : 应用的启动时间,包括创建进程+Application初始化+Activity初始化到界面显示
WaitTime : 一般比TotalTime打点,包括系统影响的耗时
-
通过Log打印计算
-
用Systrace
-
-
解决方案
-
可以使用Activity的windowBackground主题属性来为启动的Activity提供一个简单的drawable
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque"> <!-- The background color, preferably the same as your normal theme --> <item android:drawable="@android:color/white"/> <!-- Your product logo - 144dp color version of your app icon --> <item> <bitmap android:src="@drawable/logo" android:gravity="center"/> </item> </layer-list>
-
在Application类里面,做一些初始化操作,包括友盟、百度、bugly、数据库、IM、图片加载库,网络请求,广告SDK,地图等等,任务太重了,启动慢,这些非必要的业务可以通过异步加载,非第一时间需要的业务可以在主线程上做延时加载启动,当程序已经启动后再初始化。例如可以利用闪屏页的这段时间对一些SDK初始化。
-
五、包体积优化
- APK解压目录介绍
- assets文件夹 存放一些配置文件、资源文件,assets不会自动生成对应的ID,而是通过AssetManager类的接口获取
- res目录 存放资源文件,会自动生成对应的ID并映射到 .R 文件中,访问直接使用资源ID
- META-INF 保存应用的签名信息,签名信息可以验证APK的完整性
- AndroidManifest.xml Android应用的配置信息,组件注册,使用权限
- classes.dex Dalvik字节码程序,让Dalvik虚拟机可执行, 通过dx工具将Java字节码转换为 Dalvik 字节码
- resources.arsc 记录着资源文件和 资源ID之间的映射关系,用来根据资源 ID 寻找资源
- lint工具、xml写Drawable代替UI、重用资源(tint tintMode ColorFilter) 、压缩PNG(pngcrush、pnguant、zopflipng)、使用WebP文件格式、使用矢量图形、代码混淆、插件化等等
六、耗电优化
耗电分析工具 Battery Historian
- 谷歌推荐使用JobScheduler来调整任务优先级等策略来达到降低损耗的目的,JobScheduler可以避免频繁的唤醒硬件模块,造成不必要的电量消耗。
- 谷歌推行的一个懒惰第一法则
- 减少 你的应用程序可以删除冗余操作吗?例如它是否可以缓存下载的数据而不是重复唤醒无线电以重新下载数据?
- 推迟 应用是否需要立即执行操作?例如,它可以等到设备充电才能将数据备份到云端吗?
- 合并 可以批处理工作,而不是多次将设备被置与活动状态吗?例如,几十个应用程序是否真的有必要在不同时间打开收音机发送邮件?在一次唤醒收音机期间,是否可以传输消息?
七、ListView和Bitmap优化
-
对图片和质量进行压缩
public static Bitmap compressImage(Bitmap bitmap){ ByteArrayOutputStream baos = new ByteArrayOutputStream(); //质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中 bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos); int options = 100; //循环判断如果压缩后图片是否大于50kb,大于继续压缩 while ( baos.toByteArray().length / 1024>50) { //清空baos baos.reset(); bitmap.compress(Bitmap.CompressFormat.JPEG, options, baos); options -= 10;//每次都减少10 } //把压缩后的数据baos存放到ByteArrayInputStream中 ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray()); //把ByteArrayInputStream数据生成图片 Bitmap newBitmap = BitmapFactory.decodeStream(isBm, null, null); return newBitmap; }
-
对图片尺寸进行压缩
/** * 按图片尺寸压缩 参数是bitmap * @param bitmap * @param pixelW * @param pixelH * @return */ public static Bitmap compressImageFromBitmap(Bitmap bitmap, int pixelW, int pixelH) { ByteArrayOutputStream os = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os); if( os.toByteArray().length / 1024>512) {//判断如果图片大于0.5M,进行压缩避免在生成图片(BitmapFactory.decodeStream)时溢出 os.reset(); bitmap.compress(Bitmap.CompressFormat.JPEG, 50, os);//这里压缩50%,把压缩后的数据存放到baos中 } ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray()); BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; options.inPreferredConfig = Bitmap.Config.RGB_565; BitmapFactory.decodeStream(is, null, options); options.inJustDecodeBounds = false; options.inSampleSize = computeSampleSize(options , pixelH > pixelW ? pixelW : pixelH ,pixelW * pixelH ); is = new ByteArrayInputStream(os.toByteArray()); Bitmap newBitmap = BitmapFactory.decodeStream(is, null, options); return newBitmap; } /** * 动态计算出图片的inSampleSize * @param options * @param minSideLength * @param maxNumOfPixels * @return */ public static int computeSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) { int initialSize = computeInitialSampleSize(options, minSideLength, maxNumOfPixels); int roundedSize; if (initialSize <= 8) { roundedSize = 1; while (roundedSize < initialSize) { roundedSize <<= 1; } } else { roundedSize = (initialSize + 7) / 8 * 8; } return roundedSize; } private static int computeInitialSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) { double w = options.outWidth; double h = options.outHeight; int lowerBound = (maxNumOfPixels == -1) ? 1 : (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels)); int upperBound = (minSideLength == -1) ? 128 :(int) Math.min(Math.floor(w / minSideLength), Math.floor(h / minSideLength)); if (upperBound < lowerBound) { return lowerBound; } if ((maxNumOfPixels == -1) && (minSideLength == -1)) { return 1; } else if (minSideLength == -1) { return lowerBound; } else { return upperBound; } }
-
ListView列表类可以分段加载,分页显示
-
使用libjpeg.so进行压缩
八、响应速度、线程优化
- 使用线程池,异步加载
九、其他优化
- 避免创建不必要的对象
- 首选静态,如果不需要访问对象的字段,请使方法保持静态。调用速度将提高约15% - 20%,这也是很好的做法,因为你可以从方法签名中看出,调用方法不能改变对象状态
- 对常量使用static final 此优化仅适用于基本类型和String常量,不适用于任意引用类型
- 使用增强for循环语法(for-each) 可用于实现Iterable接口和数组的集合。对于集合,分配一个迭代器来对hasNext()和惊醒接口调用next()。使用一个ArrayList,手写计数循环快约3本。
- 避免使用浮点数,根据经验,浮点数比Android设备上的证书慢约2倍