作为性能优化专栏的第五篇,我们就来说一说 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)运行自己的项目,查看是否有界面有卡顿

android backtrace 分析 android anr分析工具_android


如果你的界面有上面的信息,就可以根据信息去解决自己项目中的卡顿界面。但是这种方案有一些问题:确实卡顿了,但是卡顿堆栈可能不准确;和 OOM 一样,最后的堆栈只是表象,不是真正的问题。优化:获取监控周期内的多个堆栈,而不仅是最后一个。具体说就是可以在启动 App 后就监控,监控中期内高频采集堆栈信息,当发生卡顿后,对高频采集的一个堆栈信息进行hash排重操作,然后上报给服务器。


二、ANR 分析与实战

1. ANR 介绍

  • KeyDispatchTimeout:触摸时间在一定时间没有响应,一般是 5 s。
  • BroadcastTimeoutBroadcastReceiver 在特定的时间没有完成响应。前台是 10 s,后台是 60 s。
  • ServiceTimeoutService 在特定的时间没有完成响应。前台是 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 卡顿优化实战 就说完了,各位小伙伴可以在项目中使用此方式优化。