卡顿问题错综复杂:代码 内存 CPU 绘制 IO 不易复现
线上卡顿不易复现,
1. CPU profiler
默认profiler里可以实时监控APP的CPU 情况。
也可以通过代码形式,最后生成一个文件
Android/data/packagename/files
Debug.startMethodTracing("filepath");
Debug.stopMethodTracing();
优缺点:
以图形的形式展示执行时间,调用栈
信息全面,包含所有线程
运行时开销严重,整体会变慢
2 systrace
监控跟踪API调用,生成一个html文件。
轻量级,开销小。
直观反映CPU利用率,API 18 以上,推荐使用TraceCompat
会给出建议
3 strictMode
严苛模式,Android提供的一种运行时检测机制。帮助开发人员不规范的问题。
方便强大
主要检测:
线程策略: 自定义耗时、网络 和
虚拟机策略: activity泄漏 service泄漏 实例数量 数据库泄漏
private void initStrictMode() {
if (DEV_MODE) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectCustomSlowCalls() //API等级11,使用StrictMode.noteSlowCode
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()// or .detectAll() for all detectable problems
.penaltyLog() //在Logcat 中打印违规异常信息
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.setClassInstanceLimit(NewsItem.class, 1) //限制newsItem对象为一个对象,如果出现多个对象会显示到logcat里
.detectLeakedClosableObjects() //API等级11
.penaltyLog()
.build());
}
}
logcat 里使用过滤字段为 StrictMode
4.自动检测卡顿
系统工具适合线下分析
线上及测试环节
4.1 原理:
消息处理机制,一个线程只有一个looper,在loop方法for循环里,会判断logging对象是否为null,不为null 打印当前的堆栈信息。
logging是一个Printer对象,我们设置自己的printer对象,通过Looper.getLooper().setMessageLogging(printer), mLogging对象在每个message处理前后被调用。主线程发生卡顿,是在dispatchMessage执行耗时操作。
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next(); // might block
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
}
}
//给mlogging对象赋值
public void setMessageLogging(@Nullable Printer printer) {
mLogging = printer;
}
4.2 BlockCanary
https://github.com/markzhai/AndroidPerformanceMonitor
非侵入式的性能监控组件,通知形式弹出卡顿信息。
- 依赖
implementation 'com.github.markzhai:blockcanary-android:1.5.0'
- Maximum log count is set to 500, you can rewrite it in your app int.xml.可以设置最大日志数量,重写int.xml文件
<integer name="block_canary_max_stored_count">1000</integer>
- Monitor app’s label and icon can be configured by placing a block_canary_icon drawable in your xhdpi drawable directory and in strings.xml:
在xhdpi 的strings.xml文件里添加label标签
<string name="block_canary_display_activity_label">Blocks</string>
- 在application里注册BloackCanary功能
public class DemoApplication extends Application {
@Override
public void onCreate() {
// ...
// Do it on main process
BlockCanary.install(this, new AppBlockCanaryContext()).start();
}
}
- 定义子类AppBlockCanaryContext实现 BlockCanaryContext类
Implement your application BlockCanaryContext context (strongly recommend you to check all these configs):
问题:
这种方式卡顿堆栈可能不准确,不能真正表达卡顿的具体位置。
获取监控周期内的多个堆栈,而不仅是最后一个。
startMonitor --> 高频采集堆栈–>endMonitor -->记录多个堆栈–>上报
高频卡顿上报量大,对服务器造成压力。一个卡顿下多个堆栈大概率有重复,可以对一个卡顿下堆栈进行hash排重,找出重复的堆栈。
5. ANR 分析
keyDispatchTimeOut 5s
BroadcastTimeOut 前台10s 后台60s
ServiceTimeOut 前台20s 后台200s
发生ANR ,进程会接收到异常终止信号,开始写入进程ANR信息写入到文件内(cpu IO 锁),弹出ANR提示框。
解决方案
adb pull data/anr/trances.txt 导入ANR的错误日志
5.1 线上ANR - WatchDog监控
通过FileObserver 监控文件变化,高版本权限问题
我们可以使用ANR-watchDog 一种非侵入式
A simple watchdog that detects Android ANRs (Application Not Responding).
https://github.com/SalomonBrys/ANR-WatchDog
- In the app/build.gradle file, add:
implementation 'com.github.anrwatchdog:anrwatchdog:1.4.0'
- In your application class, in onCreate, add:
new ANRWatchDog().start();
WatchDog的原理
watchDog 和 BlockCanary
BlockCanary 是监控handler内部处理的msg
ANR-WatchDog:通过改变变量值,最终根据变量的变化,来推导最终结果。
6. 卡顿单点问题方案
自动化监测方案的阈值可能没有达到直接崩溃的阈值,但是客户直观感觉卡顿。这就需要单项监测和解决方案。
IPC调用类型
调用次数 耗时
ARTHook 可以Hook 系统方法
Aspectj:非系统方法,自己的方法,或者jar
7. 界面秒开
异步+延迟初始化
异步Inflate 、x2c、绘制优化
onCreate 到 onWindowFocusChanged 从界面打开到显示的过程
Lancet
轻量级 aop框架 编译速度快,支持增量编译,使用简单,没有任何多余代码插入apk
https://github.com/eleme/lancet
classpath 'me.ele:lancet-plugin:1.0.6'
apply plugin: 'me.ele.lancet'
dependencies {
provided 'me.ele:lancet-base:1.0.6'
}
创建类ActivityHooker,添加方法
@Proxy(“i”) 对应i的方法
@TargetClass(“android.util.Log”) 对应监听Log类
@Proxy("i")
@TargetClass("android.util.Log")
public static int i(String tag, String msg) {
msg = msg + " some msg";
return (int) Origin.call();//调用原有的i方法
}
如果想监听界面打开需要的时间。需要写两个方法onCreate 和onWindowFocusChanged
@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();
}
@Proxy and @Insert
Value in @Proxy and @Insert is the target method name.
@Proxy means to hook every invoke point of the target method.
@Insert means to hook the code inside the method.
8. 定制Handler
重写sendMessage()和dispatchMessage方法
使用自己的handler ,重写两个方法,这样发送的消息和接收的消息,都可以跟踪,时间。堆栈信息,包名
public class GetDetailHandlerHelper {
private static ConcurrentHashMap<Message, String> sMsgDetail = new ConcurrentHashMap<>();
public static ConcurrentHashMap<Message, String> getMsgDetail() {
return sMsgDetail;
}
}
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) {
}
}
}
}