作为性能优化专栏的第五篇,我们就来说一说 App 卡顿优化的各种解决方案。
一、AndroidPerformanceMonitor
- 非侵入式的性能监控组件,通知形式弹出卡顿信息。
-
github
地址:AndroidPerformanceMonitor
1)添加依赖
implementation 'com.github.markzhai:blockcanary-android:1.5.0'
2)BlockCanary
配置各种信息
/**
* BlockCanary配置的各种信息
*/
public class AppBlockCanaryContext extends BlockCanaryContext {
/**
* Implement in your project.
*
* @return Qualifier which can specify this installation, like version + flavor.
*/
public String provideQualifier() {
return "unknown";
}
/**
* Implement in your project.
*
* @return user id
*/
public String provideUid() {
return "uid";
}
/**
* Network type
*
* @return {@link String} like 2G, 3G, 4G, wifi, etc.
*/
public String provideNetworkType() {
return "unknown";
}
/**
* Config monitor duration, after this time BlockCanary will stop, use
* with {@code BlockCanary}'s isMonitorDurationEnd
*
* @return monitor last duration (in hour)
*/
public int provideMonitorDuration() {
return -1;
}
/**
* Config block threshold (in millis), dispatch over this duration is regarded as a BLOCK. You may set it
* from performance of device.
*
* @return threshold in mills
*/
public int provideBlockThreshold() {
return 500;
}
/**
* Thread stack dump interval, use when block happens, BlockCanary will dump on main thread
* stack according to current sample cycle.
* <p>
* Because the implementation mechanism of Looper, real dump interval would be longer than
* the period specified here (especially when cpu is busier).
* </p>
*
* @return dump interval (in millis)
*/
public int provideDumpInterval() {
return provideBlockThreshold();
}
/**
* Path to save log, like "/blockcanary/", will save to sdcard if can.
*
* @return path of log files
*/
public String providePath() {
return "/blockcanary/";
}
/**
* If need notification to notice block.
*
* @return true if need, else if not need.
*/
public boolean displayNotification() {
return true;
}
/**
* Implement in your project, bundle files into a zip file.
*
* @param src files before compress
* @param dest files compressed
* @return true if compression is successful
*/
public boolean zip(File[] src, File dest) {
return false;
}
/**
* Implement in your project, bundled log files.
*
* @param zippedFile zipped file
*/
public void upload(File zippedFile) {
throw new UnsupportedOperationException();
}
/**
* Packages that developer concern, by default it uses process name,
* put high priority one in pre-order.
*
* @return null if simply concern only package with process name.
*/
public List<String> concernPackages() {
return null;
}
/**
* Filter stack without any in concern package, used with @{code concernPackages}.
*
* @return true if filter, false it not.
*/
public boolean filterNonConcernStack() {
return false;
}
/**
* Provide white list, entry in white list will not be shown in ui list.
*
* @return return null if you don't need white-list filter.
*/
public List<String> provideWhiteList() {
LinkedList<String> whiteList = new LinkedList<>();
whiteList.add("org.chromium");
return whiteList;
}
/**
* Whether to delete files whose stack is in white list, used with white-list.
*
* @return true if delete, false it not.
*/
public boolean deleteFilesInWhiteList() {
return true;
}
/**
* Block interceptor, developer may provide their own actions.
*/
public void onBlock(Context context, BlockInfo blockInfo) {
Log.i("lz","blockInfo "+blockInfo.toString());
}
}
3)Application
中添加如下代码
BlockCanary.install(this, new AppBlockCanaryContext()).start();
4)运行自己的项目,查看是否有界面有卡顿
如果你的界面有上面的信息,就可以根据信息去解决自己项目中的卡顿界面。但是这种方案有一些问题
:确实卡顿了,但是卡顿堆栈可能不准确;和 OOM 一样,最后的堆栈只是表象,不是真正的问题。优化
:获取监控周期内的多个堆栈,而不仅是最后一个。具体说就是可以在启动 App
后就监控,监控中期内高频采集堆栈信息,当发生卡顿后,对高频采集的一个堆栈信息进行hash排重操作,然后上报给服务器。
二、ANR 分析与实战
1. ANR 介绍
-
KeyDispatchTimeout
:触摸时间在一定时间没有响应,一般是5
s。 -
BroadcastTimeout
:BroadcastReceiver
在特定的时间没有完成响应。前台是10
s,后台是60
s。 -
ServiceTimeout
:Service
在特定的时间没有完成响应。前台是20
s,后台是200
s。
2. ANR 执行流程
- 发生 ANR。
- 进程接收异常终止信号,开始写入进程
ANR
信息。 - 弹出
ANR
提示框(Rom 表现不一)
3. ANR 解决套路
- 线下:adb pull data/anr/traces.txt 存储路径。根据里面的信息详细分析原因。
4. 线上 ANR 监控方案
- ANR-WatchDog:非侵入是的 ANR 监控组件。弥补高版本无权限问题。可以看到最终结果。
- github:ANR-WatchDog
- 原理:start -> post 消息改值 --> sleep --> 检查是否修改 --> 判断 ANR 发生。
1)导包
implementation 'com.github.anrwatchdog:anrwatchdog:1.4.0'
2)Application onCreate() 写入以下代码
new ANRWatchDog().start();
三、卡顿单点问题检测方案
1. IPC 问题监测
- 监测指标:IPC 调用类型、调用耗时,次数、调用堆栈,发生线程等。
- 常规方案:IPC 前后加埋点。缺点是不优雅,维护成本大。
- 检测技巧:adb命令。
adb shell am trace-ipc start
adb shell am trace-ipc stop --dump-file/data/local/tmp/ipc-trace.txt
- 优雅实现:ARTHook 可以 Hook 系统方法。
// Application onCreate() 方法中添加以下代码。
try {
DexposedBridge.findAndHookMethod(Class.forName("android.os.BinderProxy"), "transact",
int.class, Parcel.class, Parcel.class, int.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
LogUtils.i( "BinderProxy beforeHookedMethod " + param.thisObject.getClass().getSimpleName()
+ "\n" + Log.getStackTraceString(new Throwable()));
super.beforeHookedMethod(param);
}
});
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
四、界面秒开实现
1. 实现方案
- SysTrace ,优雅异步 + 优雅延迟初始化。
- 异步 Inflate、X2C、绘制优化。
- 提前获取页面数据。
2. 界面秒开率统计
- onCreate 到 onWindowFocusChanged 。
- Lancet:轻量级 Android AOP 框架。编译速度快,支持增量编译。API 简单,没有任何多余代码插入 apk。
- github:Lancet
1)导包
// project 的 gradle 文件
dependencies{
classpath 'me.ele:lancet-plugin:1.0.5'
}
// module 的 gradle 文件
apply plugin: 'me.ele.lancet'
dependencies {
provided 'me.ele:lancet-base:1.0.5'
}
2)编写 ActivityHook 文件
public class ActivityHooker {
public static ActivityRecord sActivityRecord;
static {
sActivityRecord = new ActivityRecord();
}
public static String trace;
@Insert(value = "onCreate",mayCreateSuper = true)
@TargetClass(value = "android.support.v7.app.AppCompatActivity",scope = Scope.ALL)
protected void onCreate(Bundle savedInstanceState) {
sActivityRecord.mOnCreateTime = System.currentTimeMillis();
Origin.callVoid();
}
@Insert(value = "onWindowFocusChanged",mayCreateSuper = true)
@TargetClass(value = "android.support.v7.app.AppCompatActivity",scope = Scope.ALL)
public void onWindowFocusChanged(boolean hasFocus) {
sActivityRecord.mOnWindowsFocusChangedTime = System.currentTimeMillis();
LogUtils.i("onWindowFocusChanged cost "+(sActivityRecord.mOnWindowsFocusChangedTime - sActivityRecord.mOnCreateTime));
Origin.callVoid();
}
public static long sStartTime = 0;
@Insert(value = "acquire")
@TargetClass(value = "com.optimize.performance.wakelock.WakeLockUtils",scope = Scope.SELF)
public static void acquire(Context context){
trace = Log.getStackTraceString(new Throwable());
sStartTime = System.currentTimeMillis();
Origin.callVoid();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
WakeLockUtils.release();
}
},1000);
}
@Insert(value = "release")
@TargetClass(value = "com.optimize.performance.wakelock.WakeLockUtils",scope = Scope.SELF)
public static void release(){
LogUtils.i("PowerManager "+(System.currentTimeMillis() - sStartTime)+"/n"+trace);
Origin.callVoid();
}
public static long runTime = 0;
@Insert(value = "run")
@TargetClass(value = "java.lang.Runnable",scope = Scope.ALL)
public void run(){
runTime = System.currentTimeMillis();
Origin.callVoid();
LogUtils.i("runTime "+(System.currentTimeMillis() - runTime));
}
@Proxy("i")
@TargetClass("android.util.Log")
public static int i(String tag, String msg) {
msg = msg + "";
return (int) Origin.call();
}
}
// ActivityRecord
public class ActivityRecord {
public long mOnCreateTime;
public long mOnWindowsFocusChangedTime;
}
五、优雅监控耗时盲区
1. 背景
- 生命周期的间隔。
-
onResume
到页面展示的间隔。
2. 监控难点
- 只知道盲区时间,不清楚具体在做什么
- 线上盲区无从追查。
3. 线下方案
- TraceView :特别适合一段时间内的盲区监控,线程具体时间做了什么,一目了然。
4. 线上方案
- 使用统一的
Handler
:定制具体方法。 - 定制
gradle
插件,编译器动态替换。lancet
public class SuperHandler extends Handler {
private long mStartTime = System.currentTimeMillis();
public SuperHandler() {
super(Looper.myLooper(), null);
}
public SuperHandler(Callback callback) {
super(Looper.myLooper(), callback);
}
public SuperHandler(Looper looper, Callback callback) {
super(looper, callback);
}
public SuperHandler(Looper looper) {
super(looper);
}
@Override
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
boolean send = super.sendMessageAtTime(msg, uptimeMillis);
if (send) {
GetDetailHandlerHelper.getMsgDetail().put(msg, Log.getStackTraceString(new Throwable()).replace("java.lang.Throwable", ""));
}
return send;
}
@Override
public void dispatchMessage(Message msg) {
mStartTime = System.currentTimeMillis();
super.dispatchMessage(msg);
if (GetDetailHandlerHelper.getMsgDetail().containsKey(msg)
&& Looper.myLooper() == Looper.getMainLooper()) {
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("Msg_Cost", System.currentTimeMillis() - mStartTime);
jsonObject.put("MsgTrace", msg.getTarget() + " " + GetDetailHandlerHelper.getMsgDetail().get(msg));
LogUtils.i("MsgDetail " + jsonObject.toString());
GetDetailHandlerHelper.getMsgDetail().remove(msg);
} catch (Exception e) {
}
}
}
}
//
public class GetDetailHandlerHelper {
private static ConcurrentHashMap<Message, String> sMsgDetail = new ConcurrentHashMap<>();
public static ConcurrentHashMap<Message, String> getMsgDetail() {
return sMsgDetail;
}
}
六、卡顿优化总结
- 耗时操作:异步、延迟。
- 布局优化:异步 Inflate、X2C
- 内存:降低内存占用,减少 GC 时间。
写在文末
好了,关于 App 卡顿优化实战
就说完了,各位小伙伴可以在项目中使用此方式优化。