1,APP性能优化
APK瘦身
- 将图片转换为 webp 格式
WebP是谷歌提供的一种支持有损压缩和无损压缩的图片文件格式,而且可以提供比JPEG或PNG更好的压缩。在Android 4.0(API level 14)中支持有损的WebP图像,在Android 4.3(API level 18)和更高版本中支持无损和透明的WebP图像。
- 除去多语言
android{
...
defaultConfig{
...
//只保留英语
resConfigs "en"
}
}
由于第三方库的引入,如appcompat-v7的引入库中包含了大量的国际化资源,可根据自身业务进行相应保留和删除。
- 去除不必要 so 库
android{
...
defaultConfig{
...
ndk {
//设置支持的SO库架构
abiFilters "armeabi-v7a"
}
}
}
1)mips / mips64: 极少用于手机可以忽略,有兴趣的可以百度一下。
2)x86 / x86_64: x86 架构的手机都会包含由 Intel 提供的称为 Houdini 的指令集动态转码工具,实现 对 arm .so 的兼容,再考虑 x86 1% 以下的市场占有率,x86 相关的两个 .so 也是可以忽略的
3)armeabi: ARM v5 这是相当老旧的一个版本,缺少对浮点数计算的硬件支持,在需要大量计算时有性能瓶颈
4)armeabi-v7a: ARM v7 目前主流版本,一般市面上的骁龙系列或者麒麟系列的处理器绝大部分都是这种架构
5)arm64-v8a: 64位支持
- 去除无用资源
1)Lint 是 Android Studio 提供的 代码扫描分析工具,它可以帮助我们发现代码结构 / 质量问题,同时提供一些解决方案,而且这个过程不需要我们手写测试用例。代码迭代版本一多,很容易会遗留一些无用的代码、资源文件,我们可以使用 Lint 进行清除。
2)shrinkResources = true
- 开启代码混淆
minifyEnabled true
- 只保留一套图
因为Android设备在加载图片时会优先加载对应分辨率文件夹下的图片,如果对应分辨率文件下没有所要的图片,则找高分辨率对应文件夹下的图片。那是不是我们把图片放在最高分辨率的文件夹下就可以了呢?不是的!因为如果这样会导致低分辨率手机加载图片时会消耗更多的内存,而且是指数级别增长的,所以如果盲目地放在一个目录是不合适的。目前不同分辨率对应优先加载的文件夹中图片如下,如果是针对国内用户的App可以只保留xxhdpi目录,而如果是东南亚市场的App则可以只保留xhdpi。
- 非重要图片动态加载
针对一些非重要的图片,可以选择动态在线加载,严格来说,非首页的图片都可以动态加载,当然,为了提升用户体验,我们会把图片放在本地。但是,一些使用场景非常小或者大小较大的图片,大胆删掉,选择动态加载吧!
内存泄露
一个长生命周期的对象持有一个短生命周期对象的引用,通俗点讲就是该回收的对象,因为引用问题没有被回收,最终会产生 OOM。
单列模式(传入Application)
Handler(Activity onDestoty() 中处理 removeCallbacksAndMessages()),非静态内部类,匿名内部类(静态内部类 + 弱引用)
静态变量(在 onDestory()中 sPolice = null,强引用 改弱引用)
定时任务(onDestory()中取消)
资源未关闭(资源用完close掉)
ps:
强引用:如果一个对象具有强引用,那垃圾回收器绝不会回收它,当内存空间不足, Java 虚拟机宁愿抛出 OOM 错误,使程序异常 Crash ,也不会靠随意回收具有强引用的对象来解决内存不足的问题.如果强引用对象不再使用时,需要弱化从而使 GC 能够回收。
软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存,只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收, java 虚拟机就会把这个软引用加入到与之关联的引用队列中。
弱引用:弱引用与软引用的区别在于: 只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用可以和一个引用队列联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
虚引用:
虚引用顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列 (ReferenceQueue) 联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
内存抖动
字符串拼接(StringBuilder)
频繁GC
onDraw()大量创建对象
耗电问题
1,不要频繁唤醒手机
2,不要频繁请求网络,良好的本地缓存策略
3,对某些执行时间较长的同步操作在用户充电且有wifi的时候在做,除非用户强制同步。
4,定位精度要求不高的情况下,使用wifi或移动网络进行定位,没有必要开启GPS定位
view过度绘制的问题
view过度绘制的问题可以说是我们在写布局的时候遇到的一个最常见的问题之一,可以说写着写着一不留神就写出了一个过度绘制,通常发生在一个嵌套的viewgroup中,比如你给他设置了一个不必要的背景。这方面问题的排查不太难,我们可以通过手机设置里面的开发者选项,打开Show GPU Overdraw的选项,轻松发现这些问题,然后尽量往蓝色靠近。
viewStub&merge的使用
这里merge和viewStub想必是大家非常了解的两个布局组件了,对于只有在某些条件下才展示出来的组件,建议使用viewStub包裹起来,同样的道理,include 某布局如果其根布局和引入他的父布局一致,建议使用merge包裹起来。
json反序列化问题
json反序列化是指将json字符串转变为对象,这里如果数据量比较多,特别是有相当多的string的时候,解析起来不仅耗时,而且还很吃内存。
App启动优化
这里还需要引入冷启动和热启动的概念,这也是我们经常会碰到的两个概念
- 冷启动:当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用,然后再根据启动的参数,启动对应的进程组件,这个启动方式就是冷启动
- 热启动:当启动应用时,后台已有该应用的进程(例:按back键、home键,应用虽然会退出,但是该应用的进程是依然会保留在后台,可进入任务列表查看),所以在已有进程的情况下,这种启动会从已有的进程中来启动对应的进程组件,这个方式叫热启动
着里提到了启动优化是指优化冷启动。
1) 启动窗口优化
启动窗口,也叫启动页、SplashWindow、StartingWindow 等,指的是应用启动时候的预览窗口。iOS App 强制有一个启动页,用户点击桌面 App 图标之后,系统会立即显示这个启动窗口,等 App 主页加载好之后再显示主页面。Android 也有类似的机制 (启动窗口这个是 Android 系统提供的),但是也提供了一个接口,让应用开发者设置是否显示这个启动窗口(默认是显示),部分开发者会把这个系统提供的启动窗口禁掉,启动自己的窗口。
但是启动自己的窗口需要的时间要比直接显示系统的启动窗口所花的时间要长,这就会导致用户在使用的时候,点击图标启动 App 的时候,有一定的延迟,表现在点击图标过了一段时间才进行窗口动画进入 App,我们要尽量避免这种情况
- 不要禁止系统默认的启动窗口:即不要在主题里面设置 android:windowDisablePreview 为 true
- 自己定制启动窗口的内容,比如将启动页主题背景设置成闪屏页图片,或者尽量使闪屏页面的主题和主页一致。可以参考知乎、抖音的做法
- 合并闪屏和主页面的 Activity :微信的做法,不过由于微信设置了 android:windowDisablePreview , 且他在各个厂商的白名单里面,一般不会被杀,冷启动的机会比较少。不过也是一个可以思考的地方。
2)线程优化
线程优化主要是减少 CPU 调度带来的波动,让启动时间更稳定。如果启动过程中有太多的线程一起启动,会给 CPU 带来非常大的压力,尤其是比较低端的机器。过多的线程同时跑会让主线程的 Sleep 和 Runnable 状态变多, 增加了应用的启动速度,优化的过程中要注意:
- 控制线程数量 – 线程池
- 检查线程间的锁 ,防止依赖等待
- 使用合理的启动架构
- 微信内部使用的 mmkernel
- 阿里 Alpha
3)主页面布局优化
应用主界面布局优化是老生常谈了,综合起来无非就是下面两点,这个需要结合具体的界面布局去做优化,网上也有比较多的资料可以查阅
- 通过减少冗余或者嵌套布局来降低视图层次结构
- 用 ViewStub 替代在启动过程中不需要显示的 UI 控件
- 使用自定义 View 替代复杂的 View 叠加
4)闲时调用
IdleHandler:当 Handler 空闲的时候才会被调用,如果返回 true, 则会一直执行,如果返回 false,执行完一次后就会被移除消息队列。比如,我们可以将从服务器获取推送 Token 的任务放在延迟 IdleHandler 中执行,或者把一些不重要的 View 的加载放到 IdleHandler 中执行。
5)业务梳理
这里涉及到具体的业务,每个 App 都不一样,但是所要做的事情都是一样的,下面是邵文在高手课里面提到的:
- 梳理清楚启动过程中的每一个模块,哪些是一定需要的,那些是可以砍掉,那些是可以懒加载的
- 根据不同的业务场景决定不同的启动模式
- 懒加载防止集中化
可以把具体的业务分为下面四个维度(此处图文来自https://juejin.im/post/5c21ea325188254eaa5c45b1#heading-5)
- 必要且耗时:启动初始化,考虑用线程来初始化
- 必要不耗时:首页绘制
- 非必要但耗时:数据上报、插件初始化
- 非必要不耗时:不用想,这块直接去掉,在需要用的时再加载
然后按需进行加载优化
ANR
ANR(Application Not Responding )应用无响应的简称,是为了在 APP卡死时,用户 可以强制退出APP的选择,从而避免卡机无响应问题,这是Android系统的一种自我保护机制。
- UI Thread超过 5 s没有响应
- Broadcast广播超过10 s没响应
- Service 服务超过 20s 没响应 因此,为避免ANR发生,请不要在主线程中进行耗时操作,耗时操作请尽量在子线程中运行。
在Android系统中,APP 通常运行在一个UI Thread或者叫MainThread里。并且Android中只有一个MainThread 和Main Message Queue。MainThread主要用于UI的绘制、事件响应,监听与接收事件处理等功能。Main Message Queue 主要存放用户要处理消息的队列,主线程MainThread从消息队列Main Message Queue中取消息Message后,尽快分发下去,一旦某条消息分发超时,则ANR可能发生。
常见的场景:
APP
正在主线程上进行缓慢的I/O
操作。APP
正在主线程中进行很复杂的计算操作- 主线程正在对另一个进程执行同步
Binder
程序调用,但另一个进程需要很长时间才能返回结果。 - 主线程在等待另一个正在长时间执行块操作的子线程时被阻塞。
- 主线程因为另一个线程死锁,无论是
Bind
调用还是主线程调用,都不能让主线程等待很久,更不能在主线程中进行复杂的计算。
2,Activity、Window、View
activity:
- Activity并不负责视图控制,它只是控制生命周期和处理事件。真正控制视图的是Window。一个Activity包含了一个Window,Window才是真正代表一个窗口。
- Activity就像一个控制器,统筹视图的添加与显示,以及通过其他回调方法,来与Window、以及View进行交互。
window:
- 表示一个窗口的概念,是所有View的直接管理者,任何视图都通过Window呈现(点击事件由Window->DecorView->View; Activity的setContentView底层通过Window完成)
- Window是一个抽象类,具体实现是PhoneWindow。PhoneWindow中有个内部类DecorView,通过创建DecorView来加载Activity中设置的布局
R.layout.activity_main
。 - 创建Window需要通过WindowManager创建,通过WindowManager将DecorView加载其中,并将DecorView交给ViewRoot,进行视图绘制以及其他交互。
- WindowManager是外界访问Window的入口
- Window具体实现位于WindowManagerService中
- WindowManager和WindowManagerService的交互是通过IPC完成
view:
- DecorView是FrameLayout的子类,它可以被认为是Android视图树的根节点视图。
- DecorView作为顶级View,一般情况下它内部包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下三个部分,上面是个ViewStub,延迟加载的视图(应该是设置ActionBar,根据Theme设置),中间的是标题栏(根据Theme设置,有的布局没有),下面的是内容栏。
3,TLS1.1,TLS1.2
自2017年1月左右开始,apple加强了iOS app安全要求,iOS 10设备上的app必须通过https且是TLS 1.2的协议去访问web资源。
WebView:
图片来源:https://en.wikipedia.org/wiki/Transport_Layer_Security#Web_browsers
从上表可以看出在Android4.4以上对TLS1.2已经完全支持,需要处理的是Android4.4以下的手机。如何处理?服务器保留TLS1.0。通过Https通信,客户端与服务端在SSL/TLS层建立安全连接前,涉及到版本协商过程。SSL/TLS在客户端和服务端分别具有对应的版本,握手阶段客户端与服务端的SSL/TLS版本,会取用两者同时支持的最高版本。如在Android 4.4手机上,默认支持SSLv3,TLSv1,如果服务端配置支持的协议版本是TLSv1,TLSv1.1,TLSv1.2,则会取用TLSv1作为协商后的版本。当然,无论是客户端还是服务端,对于SSL/TLS版本,在对应系统版本所能支持的协议版本范围内,是可以人为去修改的。如4.4系统手机上,可以将客户端在请求时的支持版本改成SSLv3,TLSv1,TLSv1.1,TLSv1.2。如果此时服务端支持的协议版本是TLSv1,TLSv1.1,TLSv1.2,协商后的版本将是TLSv1.2。
这样就会回调:
public class WebViewClient{
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
handler.proceed()
}
}
这样我们可以忽略这个错误。但是如果服务器强制TLS1.2访问就不行了。
Client:
图片来源:https://developer.android.google.cn/reference/javax/net/ssl/SSLSocket.html
从上表可以看出在Android4.4以上对TLS1.2已经完全支持,需要处理的是Android4.1~4.4的手机,它们是支持TLS1.1和TLS1.2,只是默认关闭的。如何处理?我们可以手动打开对TLS1.1和TLS1.2的支持。
ublic class TLSSocketFactory extends SSLSocketFactory{
private SSLSocketFactory internalSSLSocketFactory;
public TLSSocketFactory() throws KeyManagementException, NoSuchAlgorithmException {
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, null, null);
internalSSLSocketFactory = context.getSocketFactory();
}
public TLSSocketFactory(SSLSocketFactory delegate) throws KeyManagementException, NoSuchAlgorithmException {
internalSSLSocketFactory = delegate;
}
@Override
public String[] getDefaultCipherSuites() {
return internalSSLSocketFactory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return internalSSLSocketFactory.getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host,port,autoClose));
}
@Override
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host,port,localHost,localPort));
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort));
}
// 开启对TLS1.1和TLS1.2的支持
private Socket enableTLSOnSocket(Socket socket) {
if(socket != null && (socket instanceof SSLSocket)) {
((SSLSocket)socket).setEnabledProtocols(((SSLSocket)socket).getSupportedProtocols());
((SSLSocket)socket).setEnabledCipherSuites(((SSLSocket)socket).getSupportedCipherSuites());
}
return socket;
}
}