Github地址:新聞類App (MVP + RxJava + Retrofit+Dagger+ARouter)
卡頓介紹以及優(yōu)化工具選擇
背景介紹:
很多性能問題不易被發(fā)現(xiàn),但是卡頓很容易被直觀發(fā)現(xiàn),且卡頓難以定位
CPU Profiler
- 圖形的形式展示執(zhí)行時間,調(diào)用棧等
- 信息全面,包含所有的線程
- 缺點:運行時開銷嚴重,整體都會變慢
- 使用方式
Debug.startMethodTracing("")
Debug.stopMethodTracing("")
生成的文件在sd卡:Android/data/packagename/files
systrace
- 監(jiān)控和跟蹤 API調(diào)用,線程運行情況,生成HTML報告
- API18以上使用,推薦TraceCompat
- 使用方式
python systrace.py -t 10 [other-options] [categories]
- 優(yōu)點
輕量級,直觀反映CPU利用率,給出建議
StrictMode
- 嚴苛模式,Andorid提供的一種運行時檢測機制
- 方便強大,容易被忽視
- 包含:線程策略和虛擬機策略檢測
線程策略
自定義耗時調(diào)用,detectCustomSlowCalls();
磁盤讀取操作,detectDiskReads()
網(wǎng)絡操作,detectNetwork
虛擬機策略
Activity泄漏,detectActivityleaks()
Sqlite對象泄漏,detectleakedSqliteObjects
檢測實例數(shù)量,setClassInstanceLimit() - 代碼
private boolean DEV_MODE = true;
private void initStrictMode() {
if (!DEV_MODE) {
//線程策略
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectCustomSlowCalls()//API等級11,使用StrictMode.noteSlowCode
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()//在Logcat 中打印違規(guī)異常信息
.build());
//虛擬機策略
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
//模擬限制數(shù)量1
.setClassInstanceLimit(NewsTimeLine.class, 1)
.detectLeakedClosableObjects() //API等級11
.penaltyLog()
.build());
}
}
自動化卡頓檢測方案及優(yōu)化
理由:
- 系統(tǒng)工具適合線下針對行分析
- 線上及測試環(huán)境需要自動化檢測方案
方案原理
- 消息處理機制,一個線程只有一個Looper
- mLogging對象在每個message處理前后被調(diào)用
- 主線程發(fā)生卡頓,是在dispatchMessage執(zhí)行耗時操作
Loop源碼
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
具體實現(xiàn)
- Looper.getMainLooper().setMessagelogging()
- 匹配>>>>> Dispatching,閾值時間后執(zhí)行任務(獲取堆棧)
- 匹配<<<<< Finished,任務啟動之前取消掉
AndroidPerformanceMonitor
- 非侵入式的性能監(jiān)控組件,通知形式彈出卡頓信息
- Github:https://github.com/markzhai/AndroidPerformanceMonitor
- 添加依賴
compile 'com.github.markzhai:blockcanary-android:1.5.0'
- 代碼
App的onCreate中
BlockCanary.install(this, new AppBlockCanaryContext()).start();
AppBlockCanaryContext github中作者提供了
public class AppBlockCanaryContext extends BlockCanaryContext {
@Override
public String provideQualifier() {
return "unknown";
}
@Override
public String provideUid() {
return "uid";
}
@Override
public String provideNetworkType() {
return "unknown";
}
@Override
public int provideMonitorDuration() {
return -1;
}
@Override
public int provideBlockThreshold() {
return 500;
}
@Override
public int provideDumpInterval() {
return provideBlockThreshold();
}
@Override
public String providePath() {
return "/blockcanary/";
}
@Override
public boolean displayNotification() {
return true;
}
@Override
public boolean zip(File[] src, File dest) {
return false;
}
@Override
public void upload(File zippedFile) {
throw new UnsupportedOperationException();
}
@Override
public List<String> concernPackages() {
return null;
}
@Override
public boolean filterNonConcernStack() {
return false;
}
@Override
public List<String> provideWhiteList() {
LinkedList<String> whiteList = new LinkedList<>();
whiteList.add("org.chromium");
return whiteList;
}
@Override
public boolean deleteFilesInWhiteList() {
return true;
}
@Override
public void onBlock(Context context, BlockInfo blockInfo) {
Log.i("lz","blockInfo "+blockInfo.toString());
}
}
在fragment中添加睡眠兩秒
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
結(jié)果

image.png
點擊第一個

image.png
-優(yōu)勢:非侵入式,方便精確到哪一行
- 缺點:確實卡頓了,但卡頓堆??赡懿粶蚀_,和OOM一樣,最后的堆棧只是表象,不是真正的問題
- 優(yōu)化
獲取監(jiān)控周期內(nèi)的多個堆棧,而不僅僅是最后一個
startMonitor->高頻采集堆棧->endMonitior->記錄多個堆棧->上報 - 海量卡頓堆棧處理
分析:一個卡頓下多個堆棧大概率有重復
解決:對一個卡頓下堆棧進行hash排查,找出重復的堆棧
效果:極大的減少展示量同時更高效的找到卡頓堆棧
ANR分析與實戰(zhàn)
ANR產(chǎn)生的條件
- 1.主線程
- 2.超時時間
產(chǎn)生ANR的上下文不同,超時時間也會不同 - 3、輸入事件/特定操作
輸入事件是指按鍵、觸屏等設備輸入事件
特定操作是指BroadcastReceiver和Service的生命周期中的各個函數(shù)
ANR產(chǎn)生的情況
- 1、主線程對輸入事件在5秒內(nèi)沒有處理完畢
- 2、主線程在執(zhí)行BroadcastReceiver的onReceive函數(shù)時10秒內(nèi)沒有執(zhí)行完,注意前臺10s,后臺60s
- 3、主線程在執(zhí)行Service的各個生命周期函數(shù)時20秒內(nèi)沒有執(zhí)行完畢,注意前臺20s,后臺200s
ANR執(zhí)行流程
- 發(fā)生ANR
- 進程接受異常終止信號,開始寫入進程ANR信息
- 彈出ANR提示框
分析ANR
ANR信息保存在在/data/anr/traces.txt中

image.png
將目錄下的文件導出
Traces.txt文件分析
//文件中輸出的第一個進程的trace信息,正是發(fā)生ANR的程序
//開頭顯示進程號、ANR發(fā)生的時間點和進程名稱
----- pid 2226 at 2019-01-08 22:02:22 -----
Cmd line: com.peakmain.testproject
以下是各個線程的函數(shù)堆棧信息

image.png
//依次是:線程名、線程優(yōu)先級、線程創(chuàng)建時的序號、線程當前狀態(tài)
"main" prio=5 tid=1 Sleeping
//主線程信息
at java.lang.Thread.sleep!(Native method)
- sleeping on <0x0bf9c149> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:371)
- locked <0x0bf9c149> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:313)
at com.peakmain.testproject.MainActivity$2.onClick(MainActivity.java:61)
其他線程信息

image.png
ANR解決套路
- adb pull data/anr/trance.txt存儲路徑,可以直接導出trace文件
- 1、主線程需要做耗時操作的時候必須啟動子線程處理
- 2、子線程盡量使用android提供的API,比如HandlerThread,AsyncTask
- 3、Broadcast Receiver中如果有耗時操作,可以放到service中
ANR-WatchDog
- 非侵入式的ANR監(jiān)控組件
- Github:https://github.com/SalomonBrys/ANR-WatchDog
- 依賴
compile 'com.github.anrwatchdog:anrwatchdog:1.4.0'
- 代碼
App的onCreate中
new ANRWatchDog().start();
- 原理
start->post消息改值->sleep->檢測是否修改->判斷ANR是否發(fā)生
卡頓單點問題檢測方案
IPC問題檢測
- IPC調(diào)用類型
- 調(diào)用耗時,次數(shù)
- 調(diào)用堆棧,發(fā)生線程
常規(guī)方案
IPC前后埋點,缺點:不夠優(yōu)雅,而且維護成本高
IPC問題檢測技巧
- adb命令
adb shell am trace -ipc start
adb shell am trace -ipc stop ——dump-file /data/local/tmp/ipc-trace.txt
adb pull /data/local/tmp/ipc-trace.txt
優(yōu)雅的方案:ARTHook
- 掛鉤,將額外的代碼鉤住原有的方法,修改執(zhí)行邏輯
- 框架:Epic(不能帶到線上環(huán)境)
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();
}
實現(xiàn)界面秒開
- onCreate到onWindowFocusChanged消耗的時間
Lancet
- 編譯速度快,支持增量更新
- API簡單,沒有任何多余代碼插入apk
- Github:https://github.com/eleme/lancet
- API介紹
@Proxy通常用于對系統(tǒng)API調(diào)用的Hook
@Insert通常用于操作App與libray的類 - 依賴
classpath 'me.ele:lancet-plugin:1.0.4'
apply plugin: 'me.ele.lancet'
dependencies {
provided 'me.ele:lancet-base:1.0.4'
}
- 代碼
public class ActivityHooker {
public static ActivityRecord sActivityRecord;
static {
sActivityRecord=new ActivityRecord();
}
@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();
Log.i("ActivityHooker","onWindowFocusChanged cost "+(sActivityRecord.mOnWindowsFocusChangedTime - sActivityRecord.mOnCreateTime));
Origin.callVoid();
}
/**
* hook系統(tǒng)方法
*/
@Proxy("i")
@TargetClass("android.util.Log")
public static int i(String tag, String msg) {
msg = msg + "ActivityHooker";
return (int) Origin.call();
}
}
監(jiān)控耗時盲區(qū)
背景:
- 生命周期的間隔
- onResume到feed(界面數(shù)據(jù))展示的間隔
- 舉例:postmessage,很可能在feed之前顯示
- 線下方案:tranceView
- 線上方案
1.主線程所有方法都經(jīng)過msg,但是沒有msg具體堆棧
2.使用統(tǒng)一的Handler:定制具體的方法,發(fā)送消息都會走到sendMessageAtTime和處理消息都會走到dispatchMessage方法
public class PeakmainHandler extends Handler {
private long mStartTime = System.currentTimeMillis();
private ConcurrentHashMap<Message, String> sMsgDetail = new ConcurrentHashMap<>();
public PeakmainHandler() {
super(Looper.myLooper(), null);
}
public PeakmainHandler(Callback callback) {
super(Looper.myLooper(), callback);
}
public PeakmainHandler(Looper looper, Callback callback) {
super(looper, callback);
}
public PeakmainHandler(Looper looper) {
super(looper);
}
@Override
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
boolean send = super.sendMessageAtTime(msg, uptimeMillis);
if (send) {
sMsgDetail.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 (sMsgDetail.containsKey(msg)
&& Looper.myLooper() == Looper.getMainLooper()) {
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("Msg_Cost", System.currentTimeMillis() - mStartTime);
jsonObject.put("MsgTrace", msg.getTarget() + " " + sMsgDetail.get(msg));
Log.i("PeakmainHandler", "MsgDetail " + jsonObject.toString());
sMsgDetail.remove(msg);
} catch (Exception e) {
}
}
}
}
- 3.gradle定制,編譯時動態(tài)替換Handler,這里我并沒有去做gradle插件(理由:懶),只說下我的代碼實現(xiàn),這里我多寫了個方法,獲取所有的Handler
寫個類HandlerHelper,隨后在App中初始化就可以了
public class HandlerHelper {
public static void init() {
try {
//獲取系統(tǒng)的Handler的sendMessageAtTime
Class<?> handlerClass = Class.forName("android.os.Handler");
Method sendMessageAtTime = handlerClass.getDeclaredMethod("sendMessageAtTime", new Class[]{Message.class, long.class});
PeakmainHandler peakmainHandler=new PeakmainHandler();
handlerClass=peakmainHandler.getClass();
Object obj = Proxy.newProxyInstance(handlerClass.getClassLoader(), handlerClass.getInterfaces(), new HandlerProxy(handlerClass));
sendMessageAtTime.invoke(handlerClass,obj);
} catch (Exception e) {
e.printStackTrace();
}
}
private static class HandlerProxy implements InvocationHandler {
private Class<?> mHandlerClass;
public HandlerProxy(Class<?> handlerClass) {
this.mHandlerClass=handlerClass;
}
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
return method.invoke(mHandlerClass,objects);
}
}
public static List<Handler> getHandlerByApplication(Application application) {
List<Handler> list = new ArrayList<>();
try {
Class<Application> applicationClass = Application.class;
Field mLoadedApkField = applicationClass.getDeclaredField("mLoadedApk");
mLoadedApkField.setAccessible(true);
Object mLoadedApk = mLoadedApkField.get(application);
Class<?> mLoadedApkClass = mLoadedApk.getClass();
Field mActivityThreadField = mLoadedApkClass.getDeclaredField("mActivityThread");
mActivityThreadField.setAccessible(true);
Object mActivityThread = mActivityThreadField.get(mLoadedApk);
Class<?> mActivityThreadClass = mActivityThread.getClass();
Field mActivitiesField = mActivityThreadClass.getDeclaredField("mH");
mActivitiesField.setAccessible(true);
Object mH = mActivitiesField.get(mActivityThread);
// 注意這里一定寫成Map,低版本這里用的是HashMap,高版本用的是ArrayMap
list.add((Handler) mH);
} catch (Exception e) {
e.printStackTrace();
list = null;
}
return list;
}
}
使用
new PeakmainHandler().post(new Runnable() {
@Override
public void run() {
LogUtils.e("開始了");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});