Android性能分析与优化学习(五)卡顿优化

一 、卡顿介绍及优化工具选择

对用户来说:很多性能问题不易被发现,但是卡顿很容易被直观感受
对开发者来说:卡顿问题难以定位
卡顿问题难在哪里:
产生的原因错综复杂:代码、内存、绘制、IO?
不易复现:与用户当时场景相关

1、CPU Profiler

图形的形式展示执行时间、调用栈等,信息全面,包含所有线程
运行时开销严重,整体都会变慢
使用方式

Debug.startMethodTracing(“文件名”);//使用
Debug.stopMethodTracing();//结束

生成文件在sd卡:Android/data/packagename/files可以直接DeviceFileExplorer中找到
或者通过adb pull /mnt/sdcard/文件名.trace 指定文件目录中.将trace导出指定的文件夹中

2、Systrace
python systrace.py -t 10 [other-options][categories] python脚本

优点:轻量级,开销小,直观反映CPU使用率,给出建议。
https://developer.android.google.cn/studio/command-line/systrace

3、StrictMode

严苛模式,Android提供的一种运行时检测机制,方便强大,包含:线程策略和虚拟机策略检测
线程策略
自定义的耗时调用:detectCustomSlowCalls
磁盘读写操作:detectDiskReads、detectDiskReads
网络操作:detectDiskWrites
虚拟机策略
Activity泄漏:detectActivityLeaks
sqlite对象泄漏:detectLeakedSqlLiteObjects
检测实例数量:setClassInstanceLimit

 private void initStrictMode() {
     if (BuildConfig.DEBUG) {
         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)
                 .detectLeakedClosableObjects() //API等级11
                 .penaltyLog()
                 .build());
     }
 }

二 、自动化卡顿检测方案及优化

系统工具只适合线下针对行分析,线上及测试环境需要自动化检测方案
方案原理
消息处理机制,一个线程只有一个Looper,mLogging对象在每个message处理前后被调用
主线程发生卡顿,是在dispatchMessage执行耗时操作
具体实现
Looper.getMainLooper().setMessageLogging();设置我们的message
匹配>>>>> Dispatching,阀值时间后执行任务(获取堆栈)
匹配<<<<< Finished,任务启动之前取消掉

AndroidPerformanceMonitor

非侵入式的性能监控组件,通知形式弹出卡顿信息,非侵入式,方便精准,定位到代码某一行
https://github.com/markzhai/AndroidPerformanceMonitor
第一步:implementation ‘com.github.markzhai:blockcanary-android:1.5.0’
第二步:BlockCanary.install(this, new AppBlockCanaryContext()).start();
自动检测方案问题:
确实卡顿了,但卡顿堆栈可能不准确
和OOM一样,最后堆栈只是表象,不是真正的问题(我们是在T2时刻获取的信息,但是其实这个已经晚了,实际的卡顿发生在T2之前)
在这里插入图片描述
方案优化:
获取监控周期内的多个堆栈,而不是最后一个。
startMonitor -> 高频采集堆栈 -> endMonitor
记录多个堆栈 -> 上报
海量卡顿堆栈处理:
高频卡顿上报量太大,服务端有压力
分析:一个卡顿下有多个堆栈大概率有重复
解决:对一个卡顿下堆栈进行hash排重,找出重复的堆栈
效果:极大的减少展示量,同时更高效找到卡顿堆栈

三、 ANR分析与实战

1、ANR介绍

keyDispatchTimeOut,5s
BroadCastTimeOut,前台10s,后台60s
ServiceTimeOut,前台20s,后台200s
ANR执行流程
发生ANR -> 进行接收异常终止信号,开始写入进行anr信息 -> 弹出ANR提示框(Rom表现不一)
ANR解决套路
adb pull data/anr/traces.txt 存储路径
线上ANR监控方案
通过FileObserver监控文件变化,高版本权限问题
由于权限问题可能监控不到,这时就需要ANR-WatchDog

2、ANR-WatchDog原理及实战

非侵入式的ANR监控组件
com.github.anrwatchdog:anrwatchdog:1.3.0
https://github.com/SalomonBrys/ANR-WatchDog
第一步:implementation ‘com.github.anrwatchdog:anrwatchdog:1.3.0’
第二步:new ANRWatchDog().start()
ANRWatchDog 就是一个Thread 在线程中通过主线程的handler发送一条信息进行+1的操作,接下来这个线程Sleep一段时间,Sleep后检测+1的操作是否被执行,执行了没有卡顿,没执行就是卡顿状态。

3、区别

AndroidPerformanceMonitor:监控主线程每一个Msg的执行
WatchDog:只看最总结果
前者适合监控卡顿,后者适合补充ANR监控

四 、卡顿单点问题检测方案

自动卡顿检测方案并不够(如很多msg要执行,但是每一个msg都没有超过阀值,但是用户能感知到)
体系化解决方案务必尽早暴露问题
单点问题:主线程IPC、DB

1、IPC问题监测

(1)监测指标

  • IPC调用类型
  • 调用次数、耗时
  • 调用堆栈、发生线程
    https://blog.csdn.net/qq_26420489/article/details/52278945 IPC的6种方式 ,
    https://blog.csdn.net/zeng622peng/article/details/62881834 Android中IPC的几种方式详细分析与优缺点分析
    (2)常规方案
    IPC前后加埋点,不优雅、容易忘记,维护成本大
    (3)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

(4)优雅方案
AspectJ: 非系统方法
ARTHook:可以Hook系统方法
所有的ipc操作都走BinderProxy的 https://www.jianshu.com/p/afa794939379

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();
 }
2、单点问题监测方案

利用ARTHook完善线下工具
开发阶段Hook相关操作、暴露、分析问题
监控维度:IPC,IO、DB,View绘制

五、如何实现界面秒开

1、界面秒开实现

界面秒开就是一个小的启动优化,可以借鉴启动优化及布局优化章节

  • SysTrace:优雅异步+优雅延迟初始化(跑满cpu)
  • 异步Inflate、X2C、绘制优化
  • 提前获取页面数据(唯一路径界面)
2、界面秒开率统计

onCreate到onWindowFocusChange的时间作为统计时间或者在Activity中设置特定接口进行统计
Lancet介绍
@Proxy 通常用与对系统API调用的Hook
@Insert 常用于操作App与library的类

插件 apply plugin: ‘me.ele.lancet’
依赖 provided ‘me.ele:lancet-base:1.0.4
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();
    }

}

3、界面秒开监控纬度

总体耗时,生命周期耗时,生命后期间隔耗时

六、优雅监控耗时盲区

1、耗时盲区监控背景介绍

盲区例子:

  • 生命周期间隔
  • onResume到Feed展示的间隔
    举例:在Activity中postMessage,很可能在Feed之前执行
// 以下代码是为了演示Msg导致的主线程卡顿
new Handler().post(new Runnable() {
   @Override
   public void run() {
       LogUtils.i("Msg 执行");
       try {
           Thread.sleep(1000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }
});

耗时盲区监控难点:只知道盲区时间,不清楚具体在做什么(onResume -> FeedShwo之前干了什么,包括不同人添加的内容及第三方做的事情),线上盲区无从排查

2、耗时盲区监控方案

(1)耗时盲区监控线下方案:TraceView
特别时合一段时间内的盲区监控
线程具体时间做了什么,一目了然
(2)耗时盲区监控线上方案
思考分析

  • 所有方法都是Msg,mLogging?没有Msg具体堆栈(只知道主线程发生了Msg,但是不知道调用栈信息,它所知道的调用栈信息都是系统调用它的)
  • AOP切Handler?(只知道开始时间,不清楚准确执行时间)
    方案:
  • 使用统一的Handler:定制具体方法(sendMessageAtTime和dispatchMessage)
  • 定制gradle插件,编译期动态替换

//通过gradle将所有Handler都替换成SuperHandler
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;
    }

}


七 、卡顿优化技巧总结

1、卡顿优化实践经验

耗时操作:异步、延迟
布局优化:异步inflate、X2C、重绘解决
内存:降低内存占用,减少GC时间

2、卡顿优化工具建设

系统工具的认识、使用(Systrace-cpu使用、TraceView-每个线程在做什么、StrictMode)
自动化监控及优化(AndroidPerformanceMonitor、ANR-WatchDog、高频采集,找出重复率高的堆栈)
卡顿监控工具(单点问题:AOP、Hook,盲区监控:gradle编译期替换)
卡顿监控指标(卡顿率、ANR率、界面秒开率,交互时间、生命周期时间,上报环境、场景信息)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值