0
点赞
收藏
分享

微信扫一扫

深入卡顿优化

IT程序员 2021-09-29 阅读 136

前言

我们经常会遇到卡顿问题 而且卡顿问题往往很难解决与复现 非常的依赖卡顿现场 所以我们来深入分析一下卡顿优化

卡顿分析方法与工具

查看CPU性能

我们可以通过/proc/stat获得这个CPU的使用情况 也可以通过/proc/[pid]/stat得到某个CPU的使用情况

卡顿排查工具

  1. TraceView

    我们可以通过TraceView直观的查看每个方法的耗时 找到不符合预期的函数调用 但是TraceView可能本身开销比较大 会影响我们的判断

  2. Systrace

    我们在布局优化那边已经提到过Systrace的使用 优点是轻量级 系统级别也有很多使用Systrace 但是我们需要过滤大部分短函数

  3. CPU Profile

    Android Studio 提供了CPU Profile 来让我们直观的查看CPU的使用情况

    • Sample Java Methods 的功能类似于 Traceview 的 sample 类型。
    • Trace Java Methods 的功能类似于 Traceview 的 instrument 类型。
    • Trace System Calls 的功能类似于 systrace。
    • SampleNative (API Level 26+) 的功能类似于 Simpleperf。
  4. StrictMode

    if (BuildConfig.DEBUG) {
            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                    .detectCustomSlowCalls()
                    .detectDiskReads()
                    .detectDiskWrites()
                    .detectNetwork()// or .detectAll() for all detectable problems
                    .penaltyLog()
                    .build());
            StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                    .detectLeakedSqlLiteObjects()
                    .setClassInstanceLimit(NewsItem.class, 1)
                    .detectLeakedClosableObjects() //API等级11
                    .penaltyLog()
                    .build());
        }
    

    我们可以在Debug环境下开启严苛模式 系统会自动检测出一些异常情况 或者一些不符合预期的情况 严苛模式主要分为两种检测策略

    1. 线程策略 检测一些自定义的耗时调用 磁盘 网络io等等
    2. 虚拟机策略 检测一些数据库调用 内存泄漏 以及检测实例数量
  5. Profilo

    Profilo是FaceBook开源的一个检测卡顿信息的库
    它有以下几个优点:

    1. 集成 atrace 功能
    2. 快速获取JAVA堆栈 (我们也可以参考他的捕获方式)

线上自动化卡顿分析检测

下面详细讲一下如何做线上自动化卡顿分析

为啥要做线上卡顿分析检测?

我们可能会遇到一些反馈 应用体验太卡 抢购的时候卡了几秒? 然后我们却复现不出来 因为用户现场对卡顿很重要 所以我们需要加入线上自动化卡顿分析
在上面我们已经学习了几种工具的使用 可以方便的线下分析卡顿 接下来 我们会使用几个方法来帮助我们分析卡顿

AndroidPerformanceMonitor

我们可以使用AndroidPerformanceMonitor库来很方便检测卡顿 并且可以弹出Notification来查看卡顿堆栈

看一下使用配置

package com.dsg.androidperformance.block;

import android.content.Context;
import android.util.Log;

import com.github.moduth.blockcanary.BlockCanaryContext;
import com.github.moduth.blockcanary.internal.BlockInfo;

import java.io.File;
import java.util.LinkedList;
import java.util.List;

/**
 * @author DSG
 * @Project AndroidPerformance
 * @date 2020/7/18
 * @describe
 */
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("main1","blockInfo "+blockInfo.toString());
    }
}

我们可以看到 有很多自定义的配置项 我们可以配置一些白名单不参与检测 卡顿耗时标准等等

然后需要在Application中调用BlockCanary.install(this, new AppBlockCanaryContext()).start();就完成接入

原理分析

AndroidPerformanceMonitor的原理也很简单 就是自定义了Looper对象的Printer对象 在调用msg.target.dispatchMessage(msg);前后可以开启一个延时任务 如果dispatchMessage在延时时间里完成了 我们就认为没有发生卡顿 否则就开启子线程 生成当前堆栈信息

AndroidPerformanceMonitor源码分析

我们主要就通过BlockCanary.install(this, new AppBlockCanaryContext()).start();方法来接入
看一下start方法

 public void start() {
        if (!mMonitorStarted) {
            mMonitorStarted = true;
            Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor);
        }
    }

和我们前面讲的一样 会使用自定义的Printer对象来实现 看一下monitor对象的println方法

@Override
    public void println(String x) {
        if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
            return;
        }
        if (!mPrintingStarted) {
            mStartTimestamp = System.currentTimeMillis();
            mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
            mPrintingStarted = true;
            //开启延时任务
            startDump();
        } else {
            final long endTime = System.currentTimeMillis();
            mPrintingStarted = false;
            //是否超过阻塞时间 默认每3000毫秒就会采集一次堆栈信息
            if (isBlock(endTime)) {
                notifyBlockEvent(endTime);
            }
            //关闭
            stopDump();
        }
    }

startDump会分别启动堆采样器和cpu采样器来对任务栈进行采集 我们取cpu采样器来看一下 通过下面代码 我们可以发现 会开启一个任务来采集堆栈

 public void start() {
        if (mShouldSample.get()) {
            return;
        }
        mShouldSample.set(true);

        HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
        HandlerThreadFactory.getTimerThreadHandler().postDelayed(mRunnable,
                BlockCanaryInternals.getInstance().getSampleDelay());
    }
    
long getSampleDelay() {
        return (long) (BlockCanaryInternals.getContext().provideBlockThreshold() * 0.8f);
    }

看一下如何采集cpu信息

@Override
    protected void doSample() {
        BufferedReader cpuReader = null;
        BufferedReader pidReader = null;

        try {
            cpuReader = new BufferedReader(new InputStreamReader(
                    new FileInputStream("/proc/stat")), BUFFER_SIZE);
            String cpuRate = cpuReader.readLine();
            if (cpuRate == null) {
                cpuRate = "";
            }
              
            if (mPid == 0) {
                mPid = android.os.Process.myPid();
            }
            //手机cpu信息 我们在文章开头也讲到过
            pidReader = new BufferedReader(new InputStreamReader(
                    new FileInputStream("/proc/" + mPid + "/stat")), BUFFER_SIZE);
            String pidCpuRate = pidReader.readLine();
            if (pidCpuRate == null) {
                pidCpuRate = "";
            }
              //分析cpu信息
            parse(cpuRate, pidCpuRate);
        } catch (Throwable throwable) {
            Log.e(TAG, "doSample: ", throwable);
        } finally {
            try {
                if (cpuReader != null) {
                    cpuReader.close();
                }
                if (pidReader != null) {
                    pidReader.close();
                }
            } catch (IOException exception) {
                Log.e(TAG, "doSample: ", exception);
            }
        }
    }

我们看到会查看"/proc/" + mPid + "/stat"这个文件 但是这个文件在高版本上可能会没有权限查看

如果发生卡顿 就分析卡顿日志

setMonitor(new LooperMonitor(new LooperMonitor.BlockListener() {

            @Override
            public void onBlockEvent(long realTimeStart, long realTimeEnd,
                                     long threadTimeStart, long threadTimeEnd) {
                // Get recent thread-stack entries and cpu usage
                ArrayList<String> threadStackEntries = stackSampler
                        .getThreadStackEntries(realTimeStart, realTimeEnd);
                if (!threadStackEntries.isEmpty()) {
                    BlockInfo blockInfo = BlockInfo.newInstance()
                            .setMainThreadTimeCost(realTimeStart, realTimeEnd, threadTimeStart, threadTimeEnd)
                            .setCpuBusyFlag(cpuSampler.isCpuBusy(realTimeStart, realTimeEnd))
                            .setRecentCpuRate(cpuSampler.getCpuRateInfo())
                            .setThreadStackEntries(threadStackEntries)
                            .flushString();
                    LogWriter.save(blockInfo.toString());

                    if (mInterceptorChain.size() != 0) {
                    //遍历所有拦截器 分别调用onBlock 这里会打印日志 弹出Notification 我们还会实现自定义卡顿手机操作
                        for (BlockInterceptor interceptor : mInterceptorChain) {
                            interceptor.onBlock(getContext().provideContext(), blockInfo);
                        }
                    }
                }
            }
        }, getContext().provideBlockThreshold(), getContext().stopWhenDebugging()));

AndroidPerformanceMonitor使用总结

使用mLogging的方式 会有监控盲区的问题 所以AndroidPerformanceMonitor采用高频采集的方式分析(每1s采集一次堆栈信息)

我们在使用这个库的过程中 还是遇到了一些问题 需要我们自己去修复一下

  1. Notification在8.0以上 必须要channel id
  2. 在高版本中 /cpu/pid/stat 文件已经没有权限读取了

ANR分析

ANR发生的情况比较多 有几下几种

  1. 按键事件5s内未执行完成 KEY_DISPATCHING_TIMEOUT_MS
  2. 前台广播10s 后台广播20s未完成
  3. 前台服务20s 后台服务200s未完成
//AMS
static final int BROADCAST_FG_TIMEOUT = 10*1000;
static final int BROADCAST_BG_TIMEOUT = 60*1000;

//ATMS
KEY_DISPATCHING_TIMEOUT_MS

WatchDog源码分析

当ANR发生时 系统收到异常终止信息 写入进程ANR信息 包括当时进程的堆栈 CPU IO等情况 并且写入/data/anr目录下 我们可以通过FileObserver监听这个文件变化 查看是否发生ANR 但是在高版本中 这个文件需要ROOT权限才可以查看

所以我们可以使用WatchDog这个库来帮助我们分析手机ANR

这个库的原理也比较简单

  1. 获取当前线程的Handler 然后发送一个runnable runnable里面执行的内容就是将一个局部变量+1
  2. 等待5s后 查看局部变量是否+1 如果没有加 那么就认为发生了ANR
  3. 如果发生了ANR 就手机当前堆栈信息 并输出log 或者执行用户自定义操作

来看一下源码
ANRWatchDog继承自 Thread 所以我们来看一下run方法

@Override
    public void run() {
         //修改线程名
        setName("|ANR-WatchDog|");

        int lastTick;
        int lastIgnored = -1;
        while (!isInterrupted()) {
            lastTick = _tick;
            //往主线程post一个任务
            _uiHandler.post(_ticker);
            try {
                //睡眠5s(默认)
                Thread.sleep(_timeoutInterval);
            }
            catch (InterruptedException e) {
                //处理中断
                _interruptionListener.onInterrupted(e);
                return ;
            }

            // If the main thread has not handled _ticker, it is blocked. ANR.
            //如果没变 表示发生了ANR
            if (_tick == lastTick) {
                if (!_ignoreDebugger && Debug.isDebuggerConnected()) {
                    if (_tick != lastIgnored)
                        Log.w("ANRWatchdog", "An ANR was detected but ignored because the debugger is connected (you can prevent this with setIgnoreDebugger(true))");
                    lastIgnored = _tick;
                    continue ;
                }

                ANRError error;
                if (_namePrefix != null)
                    error = ANRError.New(_namePrefix, _logThreadsWithoutStackTrace);
                else
                    error = ANRError.NewMainOnly();//获取主线程堆栈的堆栈信息
                    //抛出异常
                _anrListener.onAppNotResponding(error);
                return;
            }
        }
    }
    
  //默认的ANR响应处理 直接抛出异常 所以遇到ANR直接就会闪退了
  private static final ANRListener DEFAULT_ANR_LISTENER = new ANRListener() {
        @Override public void onAppNotResponding(ANRError error) {
            throw error;
        }
    };

监控盲区

先来解释一下 什么是监控盲区 举个?
假如我们认为卡顿的阈值是2s 那么A方法中会调用B C方法 B方法耗时1.5s C方法耗时0.5s 这时候卡顿发生了 我们收集信息 当前任务堆栈是C方法 而不是实际的B方法 也就是监控盲区

监控盲区线下方案

线下时 我们可以直接用TraceView 直观明了 可以直接看到每个方法的耗时 可以很快的定位到耗时

监控盲区线上方案

上面我们有讲过AndroidPerformanceMonitor 这个库使用mLogging来做监控 但是只能知道系统当前任务栈 并不知道Message是被谁抛出

所以 我们可以会使用统一Handler 这样我们就可以收集sendMessageAtTimedispatchMessages方法

看一下代码

package com.optimize.performance.handler;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;

import com.optimize.performance.utils.LogUtils;

import org.json.JSONObject;

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) {
                //收集message堆栈信息
            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) {
            }
        }
    }

}

我们还会使用一个辅助类来存放msg对应堆栈信息

public class GetDetailHandlerHelper {

    private static ConcurrentHashMap<Message, String> sMsgDetail = new ConcurrentHashMap<>();

    public static ConcurrentHashMap<Message, String> getMsgDetail() {
        return sMsgDetail;
    }

}

这样我们就可以收集msg耗时和抛出msg的堆栈信息

关于全局替换Handler 我们可以使用AOP的方式来实现 可以使用滴滴出行的开源库DroidAssist

可以通过替换的方式 将所有Handler替换成我们的SuperHandler

总结

卡顿问题分析 牵扯的知识点会比较多 我们可能会学习比较吃力 但是坚持下去 收获还是会很大
在分析卡顿的过程中
我们需要线下和线上同时重点关注 线下使用ARTHook,第三方库以及TraceView 尽量在实验室环境将卡顿问题暴露出来 线上使用SuperHandlerANRWatchDog来收集卡顿和ANR信息

我们还可以通过之前讲过的启动优化 布局优化的知识点来优化卡顿问题 可以将一些耗时操作延时或者异步执行 使用异步Inflate X2C 预加载数据减少IO等待等方法 来优化卡顿问题

但是要优雅的优化代码

举报

相关推荐

0 条评论