blockcanary UI卡顿优化框架源码解析

来源:互联网 发布:八字奶 知乎 编辑:程序博客网 时间:2024/06/07 05:55

BlockCanary是国内开发者MarkZhai开发的一套性能监控组件,它对主线程操作进行了完全透明的监控,并能输出有效的信息,帮助开发分析、定位到问题所在,迅速优化应用。

其特点有:

  • 非侵入式,简单的两行就打开监控,不需要到处打点,破坏代码优雅性。
  • 精准,输出的信息可以帮助定位到问题所在(精确到行),不需要像Logcat一样,慢慢去找。目前包括了核心监控输出文件,以及UI显示卡顿信息功能

1.基本使用

使用非常方便,引入

dependencies {    compile 'com.github.markzhai:blockcanary-android:1.5.0'    // 仅在debug包启用BlockCanary进行卡顿监控和提示的话,可以这么用    debugCompile 'com.github.markzhai:blockcanary-android:1.5.0'    releaseCompile 'com.github.markzhai:blockcanary-no-op:1.5.0'}

在应用的application中完成初始化

public class DemoApplication extends Application {    @Override    public void onCreate() {        super.onCreate();        BlockCanary.install(this, new AppContext()).start();    }}//参数设置public class AppContext extends BlockCanaryContext {    private static final String TAG = "AppContext";    @Override    public String provideQualifier() {        String qualifier = "";        try {            PackageInfo info = DemoApplication.getAppContext().getPackageManager()                    .getPackageInfo(DemoApplication.getAppContext().getPackageName(), 0);            qualifier += info.versionCode + "_" + info.versionName + "_YYB";        } catch (PackageManager.NameNotFoundException e) {            Log.e(TAG, "provideQualifier exception", e);        }        return qualifier;    }    @Override    public int provideBlockThreshold() {        return 500;    }    @Override    public boolean displayNotification() {        return BuildConfig.DEBUG;    }    @Override    public boolean stopWhenDebugging() {        return false;    }}

2、基本原理

我们都知道Android应用程序只有一个主线程ActivityThread,这个主线程会创建一个Looper(Looper.prepare),而Looper又会关联一个MessageQueue,主线程Looper会在应用的生命周期内不断轮询(Looper.loop),从MessageQueue取出Message 更新UI。

我们来看一个代码片段

public static void loop() {    ...    for (;;) {        ...        // This must be in a local variable, in case a UI event sets the logger        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);        }        ...    }}

msg.target其实就是Handler,看一下dispatchMessage的逻辑

/** * Handle system messages here. */ public void dispatchMessage(Message msg) {     if (msg.callback != null) {         handleCallback(msg);     } else {         if (mCallback != null) {             if (mCallback.handleMessage(msg)) {                 return;             }         }         handleMessage(msg);     } }
  • 如果消息是通过Handler.post(runnable)方式投递到MQ中的,那么就回调runnable#run方法;
  • 如果消息是通过Handler.sendMessage的方式投递到MQ中,那么回调handleMessage方法;

不管是哪种回调方式,回调一定发生在UI线程。因此如果应用发生卡顿,一定是在dispatchMessage中执行了耗时操作。我们通过给主线程的Looper设置一个Printer,打点统计dispatchMessage方法执行的时间,如果超出阀值,表示发生卡顿,则dump出各种信息,提供开发者分析性能瓶颈。

@Overridepublic void println(String x) {    if (!mStartedPrinting) {        mStartTimeMillis = System.currentTimeMillis();        mStartThreadTimeMillis = SystemClock.currentThreadTimeMillis();        mStartedPrinting = true;    } else {        final long endTime = System.currentTimeMillis();        mStartedPrinting = false;        if (isBlock(endTime)) {            notifyBlockEvent(endTime);        }    }}private boolean isBlock(long endTime) {    return endTime - mStartTimeMillis > mBlockThresholdMillis;}

3、源码分析

源码分析主要分为框架初始化过程和监控过程

3.1 框架初始化过程

初始化过程主要通过下面第一行代码发起

BlockCanary.install(this, new AppContext()).start();

在内部我们细分为install和start过程

3.1.1 install

public static BlockCanary install(Context context, BlockCanaryContext blockCanaryContext) {    BlockCanaryContext.init(context, blockCanaryContext);    setEnabled(context, DisplayActivity.class, BlockCanaryContext.get().displayNotification());    return get();}private static void setEnabled(Context context,                               final Class<?> componentClass,                               final boolean enabled) {    final Context appContext = context.getApplicationContext();    executeOnFileIoThread(new Runnable() {        @Override        public void run() {            setEnabledBlocking(appContext, componentClass, enabled);        }    });}private static void setEnabledBlocking(Context appContext,Class<?> componentClass,boolean enabled) {    ComponentName component = new ComponentName(appContext, componentClass);    PackageManager packageManager = appContext.getPackageManager();    int newState = enabled ? COMPONENT_ENABLED_STATE_ENABLED : COMPONENT_ENABLED_STATE_DISABLED;    // Blocks on IPC.    packageManager.setComponentEnabledSetting(component, newState, DONT_KILL_APP);}
  • BlockCanaryContext.init会将保存应用的applicationContext和用户设置的配置参数;
  • setEnabled将根据用户的通知栏消息配置开启(displayNotification=true)或关闭(displayNotification=false)DisplayActivity (DisplayActivity是承载通知栏消息的activity)

注意该设置过程需要提交到一个单线程的IO线程池去执行。
接下来是外观类BlockCanary的创建过程

public static BlockCanary get() {    if (sInstance == null) {        synchronized (BlockCanary.class) {            if (sInstance == null) {                sInstance = new BlockCanary();            }        }    }    return sInstance;}//私有构造函数private BlockCanary() {    BlockCanaryInternals.setContext(BlockCanaryContext.get());    mBlockCanaryCore = BlockCanaryInternals.getInstance();    mBlockCanaryCore.addBlockInterceptor(BlockCanaryContext.get());    if (!BlockCanaryContext.get().displayNotification()) {        return;    }    mBlockCanaryCore.addBlockInterceptor(new DisplayService());}
  • 单例创建BlockCanary
  • 核心处理类为BlockCanaryInternals
  • 为BlockCanaryInternals添加拦截器(责任链)

    • BlockCanaryContext对BlockInterceptor是空实现,可以忽略;
    • DisplayService只在开启通知栏消息的时候添加,当卡顿发生时将通过DisplayService发起通知栏消息

    接下来看核心类BlockCanaryInternals的初始化过程。

public BlockCanaryInternals() {    stackSampler = new StackSampler(            Looper.getMainLooper().getThread(),            sContext.provideDumpInterval());    cpuSampler = new CpuSampler(sContext.provideDumpInterval());    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) {                    for (BlockInterceptor interceptor : mInterceptorChain) {                        interceptor.onBlock(getContext().provideContext(), blockInfo);                    }                }            }        }    }, getContext().provideBlockThreshold(), getContext().stopWhenDebugging()));    LogWriter.cleanObsolete();}

创建了两个采样类StackSampler和CpuSampler,即线程堆栈采样和CPU采样。
随后创建一个LooperMonitor,LooperMonitor实现了android.util.Printer接口。
随后通过调用setMonitor把创建的LooperMonitor赋值给BlockCanaryInternals的成员变量monitor。

3.1.2 start

即调用BlockCanary的start方法

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

将在BlockCanaryInternals中创建的LooperMonitor给主线程Looper的mLogging变量赋值。这样主线程Looper就可以消息分发前后使用LooperMonitor#println输出日志。

3.2 卡顿监控过程

根据上面原理的分析,监控的对象主要是Main Looper的Message分发耗时情况。

//Looperfor (;;) {    Message msg = queue.next();    // This must be in a local variable, in case a UI event sets the logger    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);    }    ...}

主线程的所有消息都在这里调度!!
每从MQ中取出一个消息,由于我们设置了Printer为LooperMonitor,因此在调用dispatchMessage前后都可以交由我们LooperMonitor接管。
我们再次从下面这段代码入手。

@Overridepublic 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;        if (isBlock(endTime)) {            notifyBlockEvent(endTime);        }        stopDump();    }}

对于单个Message而言,这个方法一定的成对调用的。

3.2.1 卡顿监控记录

第一次调用时,记录开始时间,并开始dump堆栈和CPU信息。

//LooperMonitorprivate void startDump() {    if (null != BlockCanaryInternals.getInstance().stackSampler) {        BlockCanaryInternals.getInstance().stackSampler.start();    }    if (null != BlockCanaryInternals.getInstance().cpuSampler) {        BlockCanaryInternals.getInstance().cpuSampler.start();    }}//AbstractSamplerpublic void start() {    if (mShouldSample.get()) {        return;    }    mShouldSample.set(true);    HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);    HandlerThreadFactory.getTimerThreadHandler().postDelayed(mRunnable,            BlockCanaryInternals.getInstance().getSampleDelay());}private Runnable mRunnable = new Runnable() {    @Override    public void run() {        doSample();        if (mShouldSample.get()) {            HandlerThreadFactory.getTimerThreadHandler()                    .postDelayed(mRunnable, mSampleInterval);        }    }};
  • 两种采样依次提交到HandlerThread中进行,从而保证采样过程是在一个后台线程执行;
  • 两种采样有个共同的父类AbstractSampler,采用了模板方法模式,即在父类定义了采样的抽象算法doSample及采样生命周期的管控(start和stop),不同的子类采样的算法实现是不一样的;
  • 采样会周期性执行,间隔时间与卡顿阀值一致(可由开发者设置);
3.2.1.1 堆栈采样

堆栈采样很简单,直接通过Main Looper获取到主线程Thread对象,调用Thread#getStackTrace即可获取到堆栈信息

@Overrideprotected void doSample() {    StringBuilder stringBuilder = new StringBuilder();    for (StackTraceElement stackTraceElement : mCurrentThread.getStackTrace()) {        stringBuilder                .append(stackTraceElement.toString())                .append(BlockInfo.SEPARATOR);    }    synchronized (sStackMap) {        if (sStackMap.size() == mMaxEntryCount && mMaxEntryCount > 0) {            sStackMap.remove(sStackMap.keySet().iterator().next());        }        sStackMap.put(System.currentTimeMillis(), stringBuilder.toString());    }}

将堆栈拼成String,保存在LinkedHashMap中,当然保存有一定阀值,默认最多保存100条。

3.2.1.2 CPU采样

在分析代码之前我们需要先了解一下Android平台CPU的一些常识。
我们都知道Android是基于Linux系统的,Android平台关于CPU的计算是跟Linux是完全一样的。
/proc/stat文件
在Linux中CPU活动信息是保存在该文件中,该文件中的所有值都是从系统启动开始累计到当前时刻。

~$ cat /proc/statcpu  38082 627 27594 893908 12256 581 895 0 0cpu0 22880 472 16855 430287 10617 576 661 0 0cpu1 15202 154 10739 463620 1639 4 234 0 0intr 120053 222 2686 0 1 1 0 5 0 3 0 0 0 47302 0 0 34194 29775 0 5019 845 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0ctxt 1434984btime 1252028243processes 8113procs_running 1procs_blocked 0

第二行的数值表示的是CPU总的使用情况,所以我们只要用第一行的数字计算就可以了
下表解析第一行各数值的含义

参数解析 (以下数值都是从系统启动累计到当前时刻)user (38082)处于用户态的运行时间,不包含 nice值为负进程 nice (627)nice值为负的进程所占用的CPU时间 system (27594)处于核心态的运行时间 idle (893908)除IO等待时间以外的其它等待时间iowait (12256) 从系统启动开始累计到当前时刻,IO等待时间 irq (581)硬中断时间 irq (581)软中断时间 stealstolen(0)一个其他的操作系统运行在虚拟环境下所花费的时间 guest(0)这是在Linux内核控制下为客户操作系统运行虚拟CPU所花费的时间 

总结:总的cpu时间totalCpuTime = user + nice + system + idle + iowait + irq + softirq + stealstolen + guest

/proc/pid/stat文件
该文件包含了某一进程所有的活动的信息,该文件中的所有值都是从系统启动开始累计到当前时刻

~$ cat /proc/6873/stat6873 (a.out) R 6723 6873 6723 34819 6873 8388608 77 0 0 0 41958 31 0 0 25 0 3 0 5882654 1409024 56 4294967295 134512640 134513720 3215579040 0 2097798 0 0 0 0 0 0 0 17 0 0 0

以下只解释对我们计算Cpu使用率有用相关参数

参数解析pid=6873进程号utime=1587该任务在用户态运行的时间,单位为jiffiesstime=41958该任务在核心态运行的时间,单位为jiffiescutime=0所有已死线程在用户态运行的时间,单位为jiffiescstime=0所有已死在核心态运行的时间,单位为jiffies

结论:进程的总Cpu时间processCpuTime = utime + stime + cutime + cstime,该值包括其所有线程的cpu时间。

CPU采样的代码如下:

@Overrideprotected 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();        }        pidReader = new BufferedReader(new InputStreamReader(                new FileInputStream("/proc/" + mPid + "/stat")), BUFFER_SIZE);        String pidCpuRate = pidReader.readLine();        if (pidCpuRate == null) {            pidCpuRate = "";        }        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);        }    }}private void parse(String cpuRate, String pidCpuRate) {    String[] cpuInfoArray = cpuRate.split(" ");    if (cpuInfoArray.length < 9) {        return;    }    long user = Long.parseLong(cpuInfoArray[2]);    long nice = Long.parseLong(cpuInfoArray[3]);    long system = Long.parseLong(cpuInfoArray[4]);    long idle = Long.parseLong(cpuInfoArray[5]);    long ioWait = Long.parseLong(cpuInfoArray[6]);    long total = user + nice + system + idle + ioWait            + Long.parseLong(cpuInfoArray[7])            + Long.parseLong(cpuInfoArray[8]);    String[] pidCpuInfoList = pidCpuRate.split(" ");    if (pidCpuInfoList.length < 17) {        return;    }    long appCpuTime = Long.parseLong(pidCpuInfoList[13])            + Long.parseLong(pidCpuInfoList[14])            + Long.parseLong(pidCpuInfoList[15])            + Long.parseLong(pidCpuInfoList[16]);    if (mTotalLast != 0) {        StringBuilder stringBuilder = new StringBuilder();        long idleTime = idle - mIdleLast;        long totalTime = total - mTotalLast;        stringBuilder                .append("cpu:")                .append((totalTime - idleTime) * 100L / totalTime)                .append("% ")                .append("app:")                .append((appCpuTime - mAppCpuTimeLast) * 100L / totalTime)                .append("% ")                .append("[")                .append("user:").append((user - mUserLast) * 100L / totalTime)                .append("% ")                .append("system:").append((system - mSystemLast) * 100L / totalTime)                .append("% ")                .append("ioWait:").append((ioWait - mIoWaitLast) * 100L / totalTime)                .append("% ]");        synchronized (mCpuInfoEntries) {            mCpuInfoEntries.put(System.currentTimeMillis(), stringBuilder.toString());            if (mCpuInfoEntries.size() > MAX_ENTRY_COUNT) {                for (Map.Entry<Long, String> entry : mCpuInfoEntries.entrySet()) {                    Long key = entry.getKey();                    mCpuInfoEntries.remove(key);                    break;                }            }        }    }    mUserLast = user;    mSystemLast = system;    mIdleLast = idle;    mIoWaitLast = ioWait;    mTotalLast = total;    mAppCpuTimeLast = appCpuTime;}

3.2.2 卡顿条件判断及事后处理

当LooperMonitor第二次调用时,会判断第二次与第一次的时间间隔是否会超过阀值。

private boolean isBlock(long endTime) {    return endTime - mStartTimestamp > mBlockThresholdMillis;}

若超过,将视作一次卡顿。满足卡顿条件将会调用下面方法

private void notifyBlockEvent(final long endTime) {    final long startTime = mStartTimestamp;    final long startThreadTime = mStartThreadTimestamp;    final long endThreadTime = SystemClock.currentThreadTimeMillis();    HandlerThreadFactory.getWriteLogThreadHandler().post(new Runnable() {        @Override        public void run() {            mBlockListener.onBlockEvent(startTime, endTime, startThreadTime, endThreadTime);        }    });}

可以看到日志的写入执行在工作线程(HandlerThread),将回调BlockListener#onBlockEvent


QQ20170305-192253@2x.png

将堆栈采样和CPU采样数据封装为一个BlockInfo。
接下来将进行卡顿事后处理。
主要有两件事情:

  • 将卡顿发生时的堆栈和CPU信息写入日志;
  • 如果开启走通知栏,那么将发出一条通知栏消息;
3.2.2.1 卡顿日志记录

通过LogWriter.save(blockInfo.toString())完成

public static String save(String str) {    String path;    synchronized (SAVE_DELETE_LOCK) {        path = save("looper", str);    }    return path;}private static String save(String logFileName, String str) {    String path = "";    BufferedWriter writer = null;    try {        File file = BlockCanaryInternals.detectedBlockDirectory();        long time = System.currentTimeMillis();        path = file.getAbsolutePath() + "/"                + logFileName + "-"                + FILE_NAME_FORMATTER.format(time) + ".log";        OutputStreamWriter out =                new OutputStreamWriter(new FileOutputStream(path, true), "UTF-8");        writer = new BufferedWriter(out);        writer.write(BlockInfo.SEPARATOR);        writer.write("**********************");        writer.write(BlockInfo.SEPARATOR);        writer.write(TIME_FORMATTER.format(time) + "(write log time)");        writer.write(BlockInfo.SEPARATOR);        writer.write(BlockInfo.SEPARATOR);        writer.write(str);        writer.write(BlockInfo.SEPARATOR);        writer.flush();        writer.close();        writer = null;    } catch (Throwable t) {        Log.e(TAG, "save: ", t);    } finally {        try {            if (writer != null) {                writer.close();            }        } catch (Exception e) {            Log.e(TAG, "save: ", e);        }    }    return path;}

注意:以上代码的调用执行在工作线程HandlerThread(writer)中

3.2.2.2 通知栏消息

通知栏消息由下面代码触发

if (mInterceptorChain.size() != 0) {    for (BlockInterceptor interceptor : mInterceptorChain) {        interceptor.onBlock(getContext().provideContext(), blockInfo);    }}

其中BlockInterceptor的一个实现类为DisplayService

final class DisplayService implements BlockInterceptor {    private static final String TAG = "DisplayService";    @Override    public void onBlock(Context context, BlockInfo blockInfo) {        Intent intent = new Intent(context, DisplayActivity.class);        intent.putExtra("show_latest", blockInfo.timeStart);        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);        PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, FLAG_UPDATE_CURRENT);        String contentTitle = context.getString(R.string.block_canary_class_has_blocked, blockInfo.timeStart);        String contentText = context.getString(R.string.block_canary_notification_message);        show(context, contentTitle, contentText, pendingIntent);    }    @TargetApi(HONEYCOMB)    private void show(Context context, String contentTitle, String contentText, PendingIntent pendingIntent) {        NotificationManager notificationManager = (NotificationManager)                context.getSystemService(Context.NOTIFICATION_SERVICE);        Notification notification;        if (SDK_INT < HONEYCOMB) {            notification = new Notification();            notification.icon = R.drawable.block_canary_notification;            notification.when = System.currentTimeMillis();            notification.flags |= Notification.FLAG_AUTO_CANCEL;            notification.defaults = Notification.DEFAULT_SOUND;            try {                Method deprecatedMethod = notification.getClass().getMethod("setLatestEventInfo", Context.class, CharSequence.class, CharSequence.class, PendingIntent.class);                deprecatedMethod.invoke(notification, context, contentTitle, contentText, pendingIntent);            } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException                    | InvocationTargetException e) {                Log.w(TAG, "Method not found", e);            }        } else {            Notification.Builder builder = new Notification.Builder(context)                    .setSmallIcon(R.drawable.block_canary_notification)                    .setWhen(System.currentTimeMillis())                    .setContentTitle(contentTitle)                    .setContentText(contentText)                    .setAutoCancel(true)                    .setContentIntent(pendingIntent)                    .setDefaults(Notification.DEFAULT_SOUND);            if (SDK_INT < JELLY_BEAN) {                notification = builder.getNotification();            } else {                notification = builder.build();            }        }        notificationManager.notify(0xDEAFBEEF, notification);    }}


作者:J_Beyondev
链接:http://www.jianshu.com/p/e58992439793
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
原创粉丝点击