1.简介
随着开发的业务越来越多,如果一开始不注意性能优化,就会造成APP越来越卡,这篇博客,就是将一些常用的性能优化方案整理出来,希望能帮到大家。
2.快
一个优秀的APP,最重要的一个特点就是快,如果点开之后卡半天,估计你也不想再用了。那么怎么才能更快?
- 启动快:点开图标就能很快的进入页面
- 加载dex优化
- 第三方框架初始化优化
- 页面快:页面加载内容快
- 相应快:事件相应快
============================== 快 =================================
启动快
1. MultiDex优化
1.1 简介
App集成一堆库之后,方法数一般都是超过65536的,解决办法就是:一个dex装不下,用多个dex来装。配置很简单
defaultConfig {
...
multiDexEnabled = true
}
dependencies {
...
implementation 'androidx.multidex:multidex:2.0.0'
}
// 在自定义的Application中加载dex
@Override
protected void attachBaseContext(Context context) {
super.attachBaseContext(context);
MultiDex.install(context)
}
但是在低于5.0的机器打印MultiDex.install(context)耗时大约为1到2秒钟,原因是
第一次加载时,会涉及到解压apk取出dex、压缩dex、将dex文件通过反射转换成DexFile对象、反射替换数组等原因。
所以如果直接这么写,会比较慢。
1.2 优化方案
- 在主进程Application 的 attachBaseContext 方法中判断如果需要使用MultiDex,则创建一个临时文件,然后开一个进程(LoadDexActivity),显示Loading,异步执行MultiDex.install 逻辑,执行完就删除临时文件并finish自己。
- 主进程Application 的 attachBaseContext 进入while代码块,定时轮循临时文件是否被删除,如果被删除,说明MultiDex已经执行完,则跳出循环,继续正常的应用启动流程。
- 注意LoadDexActivity 必须要配置在main dex中。
1.3 代码
– MyApplication.java
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
boolean isMainProcess = isMainProcess(base);
// 主进程并且vm不支持多dex的情况下才使用 MultiDex
if (isMainProcess && !SystemUtil.isVMMultidexCapable()){
loadMultiDex(base);
}
}
private void loadMultiDex(Context context) {
newTempFile(context); // 随便创建临时文件
// 启动另一个进程去加载MultiDex
Intent intent = new Intent(context, LoadMultiDexActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
// 检查MultiDex是否安装完(安装完会删除临时文件)
checkUntilLoadDexSuccess(context);
// 另一个进程以及加载 MultiDex,有缓存了,所以主进程再加载就很快了。
// 为什么主进程要再加载,因为每个进程都有一个ClassLoader
long startTime = System.currentTimeMillis();
MultiDex.install(context);
preNewActivity();
}
/**
* 对象第一次创建的时候,java虚拟机首先检查类对应的Class 对象是否已经加载。
* 如果没有加载,jvm会根据类名查找.class文件,将其Class对象载入。
* 同一个类第二次new的时候就不需要加载类对象,而是直接实例化,创建时间就缩短了。
*/
private void preNewActivity() {
long startTime = System.currentTimeMillis();
MainActivity mainActivity = new MainActivity();
}
// 创建一个临时文件,MultiDex install 成功后删除
private void newTempFile(Context context) {
try {
File file = new File(context.getCacheDir().getAbsolutePath(), "load_dex.tmp");
if (!file.exists()) {
file.createNewFile();
}
} catch (Throwable th) {
th.printStackTrace();
}
}
/**
* 检查MultiDex是否安装完, 通过判断临时文件是否被删除
*/
private void checkUntilLoadDexSuccess(Context context) {
File file = new File(context.getCacheDir().getAbsolutePath(), "load_dex.tmp");
int i = 0;
int waitTime = 100; //睡眠时间
try {
while (file.exists()) {
Thread.sleep(waitTime);
if (i > 40) {
break;
}
}
}catch (Exception e){
e.printStackTrace();
}
}
– LoadMultiDexActivity.java
public class LoadMultiDexActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_load_multi_dex);
Thread thread = new Thread() {
@Override
public void run() {
loadMultiDex();
}
};
thread.setName("multi_dex");
thread.start();
showLoadingDialog();
}
/**
* 加载multdex
*/
private void loadMultiDex(){
long startTime = System.currentTimeMillis();
MultiDex.install(LoadMultiDexActivity.this);
try {
//模拟MultiDex耗时很久的情况
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
aftetMultiDex();
}
private void aftetMultiDex() {
deleteTempFile(this);
// 将这个进程杀死
finish();
Process.killProcess(Process.myPid());
}
private void deleteTempFile(Context context) {
try {
File file = new File(context.getCacheDir().getAbsolutePath(), "load_dex.tmp");
if (file.exists()) {
file.delete();
}
} catch (Throwable th) {
th.printStackTrace();
}
}
private void showLoadingDialog(){
new AlertDialog.Builder(this)
.setMessage("加载中,请稍后...")
.show();
}
}
这样就快了很多,至于详细原理,请看蓝师傅的
面试官:今日头条启动很快,你觉得可能是做了哪些优化?
2 第三方框架初始化优化
2.1 简介
启动Activity时,我们会很习惯的把一些第三方框架的初始化或者业务的耗时操作放到onCreate()里面或者放到onResume里面,但是页面展示的流程大家也都知道是:
- ActivityThread.handleLaunchActivity() ->
- performLaunchActivity() ->
- onCreate() -> 将xml文件转换成view添加到decorView上
- ActivityThread.handleResumeActivity() ->
- onResume() ->
- 将decorView添加到windowManager上
- 创建viewRoot.addview() ->
- requestLayout()->
- performTraversals() -> 调用Measure() , layout(), draw() 完成绘制
- 显示页面
所以大家也看到了,要是耗时逻辑或者第三方初始化,放在onCreate()或者onResume()里面,它就会影响我们页面的显示,也可以理解为,影响我们的启动。
将耗时操作及一些用到的第三方框架的初始化进行延迟加载。
如果耗时操作或者资源加载是必须在主线程中使用的,我们使用的是IdleHandler。如果可以在子线程中执行的话,我们使用HandlerThread。
2.2 优化方案1(IdleHandler)
当MessageQueue中没有更多的消息的时候就会回调queueIdle()这个方法,返回true的话,当MessageQueue中没有消息的时候还会继续回调这个方法,返回false则会在执行完之后移除掉这个监听。
// 获取主线程的消息池
Looper.getMainLooper().getQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
// 耗时操作
return false;
}
});
2.3 优化方案2(HandlerThread)
HandlerThread的本质:继承Thread类 和 封装Handler类
使用
// 创建HandlerThread实例对象,并起线程名字
HandlerThread mHandlerThread = new HandlerThread("handlerThread");
// 启动线程
mHandlerThread.start();
// 关联HandlerThread的Looper对象、实现消息处理操作 以及与其他线程进行通信
Handler workHandler = new Handler( handlerThread.getLooper() ) {
@Override
public boolean handleMessage(Message msg) {
// 可处理耗时操作
return true;
}
});
// 定义要发送的消息,在工作线程执行相关操作
Message msg = Message.obtain();
msg.what = 2; //消息的标识
msg.obj = "B"; // 消息的存放
workHandler.sendMessage(msg);
// 结束线程,即停止线程的消息循环
mHandlerThread.quit();
好了,至少经过上面的改装,已经能够点开图标,很快的显示页面了,当然,如果页面比较复杂,还是慢的话,我们就接下来分析页面如何优化。
页面快
1 布局优化
页面需绘制的内容(布局 & 控件)太多,从而导致页面绘制时间过长
1.1 简介
使用<include>、<Viewstub>、<merge>简化层级,避免布局层级过多
1.2 优化方案1(<include>)
<include>: 布局重用, 可提高布局的复用性
1.3 优化方案2(<Viewstub>)
<Viewstub> :标签懒加载,