微信Matrix框架解析
整体分析
Matrix.Builder builder = new Matrix.Builder(application); // build matrix builder.patchListener(new TestPluginListener(this)); // add general pluginListener DynamicConfigImplDemo dynamicConfig = new DynamicConfigImplDemo(); // dynamic config // init plugin IOCanaryPlugin ioCanaryPlugin = new IOCanaryPlugin(new IOConfig.Builder() .dynamicConfig(dynamicConfig) .build()); //add to matrix builder.plugin(ioCanaryPlugin); //init matrix Matrix.init(builder.build()); // start plugin ioCanaryPlugin.start();
整理的结构如下:
核心功能:
- Resource Canary:
- Activity 泄漏
- Bitmap 冗余
- Trace Canary
- 界面流畅
- 启动耗时
- 页面切换耗时
- 慢函数
- 卡顿
- SQLite Lint: 按官方最佳实践自动化检测 SQLite 语句的使用质量
- IO Canary: 检测文件 IO 问题
- 文件 IO 监控
- Closeable Leak 监控
- 整体架构分析:
matrix-android-lib
- Plugin 被定义为某种监控能力的抽象
- Issue 发生的某种被监控事件
- Report 一种观察者模式的实现,用于发现Issue时,通知观察者
- Matrix 单例模式的实现,暴漏给外部的接口
- matrix-config.xml 相关监控的配置项
- IDynamicConfig 开放给用户自定义的相关监控的配置项,其实例会被各个Plugin持
- plugin核心接口
- IPlugin 一种监控能力的抽象
- PluginListener 开放给用户的,感知监控模块初始化、启动、停止、发现问题等生命周期的能力。 用户可自行实现,并注入到Matrix中
- Plugin 1、对IPlugin的默认实现,以触发相关PluginListener生命周期 2、实现 IssuePublisher.OnIssueDetectListener接口的onDetectIssue方法
- 该方法会在 具体监控事件发生时,被 Matrix.with().getPluginByClass(xxx).onDetectIssue(Issue)这样调用。注意这里是第一种通知方式
- 该方法内部对Issue相关环境变量进行赋值,譬如引发该Issue的Plugin信息
- 该方法最终触发PluginListener#onReportIssue
public interface IPlugin { /** * 用于标识当前的监控,相当于名称索引(也可用classname直接索引) */ String getTag(); /** * 在Matrix对象构建时被调用 */ void init(Application application, PluginListener pluginListener); /** * 对activity前后台转换的感知能力 */ void onForeground(boolean isForeground); void start(); void stop(); void destroy(); } public interface PluginListener { void onInit(Plugin plugin); void onStart(Plugin plugin); void onStop(Plugin plugin); void onDestroy(Plugin plugin); void onReportIssue(Issue issue); }
Matrix对外接口
public class Matrix { private static final String TAG = "Matrix.Matrix"; /********************************** 单例实现 **********************/ private static volatile Matrix sInstance; public static Matrix init(Matrix matrix) { if (matrix == null) { throw new RuntimeException("Matrix init, Matrix should not be null."); } synchronized (Matrix.class) { if (sInstance == null) { sInstance = matrix; } else { MatrixLog.e(TAG, "Matrix instance is already set. this invoking will be ignored"); } } return sInstance; } public static boolean isInstalled() { return sInstance != null; } public static Matrix with() { if (sInstance == null) { throw new RuntimeException("you must init Matrix sdk first"); } return sInstance; } /**************************** 构造函数 **********************/ private final Application application; private final HashSet<Plugin> plugins; private final PluginListener pluginListener; private Matrix(Application app, PluginListener listener, HashSet<Plugin> plugins) { this.application = app; this.pluginListener = listener; this.plugins = plugins; for (Plugin plugin : plugins) { plugin.init(application, pluginListener); pluginListener.onInit(plugin); } } /**************************** 控制能力 **********************/ public void startAllPlugins() { for (Plugin plugin : plugins) { plugin.start(); } } public void stopAllPlugins() { for (Plugin plugin : plugins) { plugin.stop(); } } public void destroyAllPlugins() { for (Plugin plugin : plugins) { plugin.destroy(); } } /**************************** get | set **********************/ public Plugin getPluginByTag(String tag) { for (Plugin plugin : plugins) { if (plugin.getTag().equals(tag)) { return plugin; } } return null; } public <T extends Plugin> T getPluginByClass(Class<T> pluginClass) { String className = pluginClass.getName(); for (Plugin plugin : plugins) { if (plugin.getClass().getName().equals(className)) { return (T) plugin; } } return null; } /**************************** 其他 **********************/ public static void setLogIml(MatrixLog.MatrixLogImp imp) { MatrixLog.setMatrixLogImp(imp); } }
Utils 辅助功能
- MatrixLog 开放给用户的log能力
- MatrixHandlerThread 公共线程,往往用于异步线程执行一些任务
- DeviceUtil 获取相关设备信息
- MatrixUtil 判断主线程等其他utils
IssuePublisher被监控事件观察者
- Issue 被监控事件 type:类型,用于区分同一个tag不同类型的上报 tag: 该上报对应的tag stack:该上报对应的堆栈 process:该上报对应的进程名 time:issue 发生的时间
IssuePublisher 观察者模式
- 持有一个 发布Listener(其实现往往是上文的 Plugin)
- 持有一个 已发布信息的Map,在一次运行时长内,避免针对同一事件的重复发布
- 一般而言,某种监控的监控探测器往往继承该类,并在检测到事件发生时,调用publishIssue(Issue)—>IssuePublisher.OnIssueDetectListener接口的onDetectIssue方法—>最终触发PluginListener#onReportIssue
Matrix的模块IO监测核心源码分析
IO Canary:核心的作用是检测文件 IO 问题,包括:文件 IO 监控和 Closeable Leak 监控。要想理解IO的监测和看懂开源的代码,最重要的基础就是掌握Native和Java层面的Hook。
Java层面的hook主要是基于反射技术,大家都比较熟悉了,那我们来聊一聊Native层面的Hook。在JVM层面,Android使用Android PLT (Procedure Linkage Table)Hook和Inline Hook、ptrace三种主流的技术。
Matrix采用PLT的技术来实现SO文件API的Hook。
ELF: en.wikipedia.org/wiki/Execut…
refspecs.linuxbase.org/elf/elf.pdf
ELF文件的三种形式:
- 可重定位的对象文件(Relocatable file),也就是我们熟悉的那些.o文件,当然.a库也算(因为它是.o的集合)
- 可执行的对象文件(Executable file),linux下的可执行文件
- 可被共享的对象文件(Shared object file),动态链接库.so
我们思考下C的程序是需要进行编译和链接再到最后的运行的。那么ELF文件从这个参与程序运行的角度也是分为2种视图的。
ELF header 位于文件的最开始处,描述整个文件的组织结构。Program Header Table 告诉系统如何创建进程镜像,在执行程序时必须存在,在 relocatable files 中则不需要。每个 program header 分别描述一个 segment,包括 segment 在文件和内存中的大小及地址等等。执行视图中的 segment 其实是由很多个 section 组成。在一个进程镜像中通常具有 text segment 和 data segment 等等。
关于重定位的作用和概念
重定位就是把符号引用与符号定义链接起来的过程,这也是 android linker 的主要工作之一。当程序中调用一个函数时,相关的 call 指令必须在执行期将控制流转到正确的目标地址。所以,so 文件中必须包含一些重定位相关的信息,linker 据此完成重定位的工作。
android.googlesource.com/platform/bi…
符号表表项的结构为elf32_sym:
typedef struct elf32_sym { Elf32_Word st_name; /* 名称 – index into string table */ Elf32_Addr st_value; /* 偏移地址 */ Elf32_Word st_size; /* 符号长度( 例如,函数的长度) */ unsigned char st_info; /* 类型和绑定类型 */ unsigned char st_other; /* 未定义 */ Elf32_Half st_shndx; /* section header的索引号,表示位于哪个section中 */ } Elf32_Sym;
重定位核心代码:
androidxref.com/8.0.0_r4/xr… (具体的重定位类型定义和计算方法可以参考elf说明文档的 4.6.1.2 小节)
Android PLT Hook的基本原理
Linux在执行动态链接的ELF的时候,为了优化性能使用了一个叫延时绑定的策略。当在动态链接的ELF程序里调用共享库的函数时,第一次调用时先去查找PLT表中相应的项目,而PLT表中再跳跃到GOT表中希望得到该函数的实际地址,但这时GOT表中指向的是PLT中那条跳跃指令下面的代码,最终会执行_dl_runtime_resolve()
并执行目标函数。因此,PLT Hook通过直接修改GOT表,使得在调用该共享库的函数时跳转到的是用户自定义的Hook功能代码。
IO 监控流程:
JNIEXPORT jboolean JNICALL Java_com_tencent_matrix_iocanary_core_IOCanaryJniBridge_doHook(JNIEnv *env, jclass type) { __android_log_print(ANDROID_LOG_INFO, kTag, "doHook"); for (int i = 0; i < TARGET_MODULE_COUNT; ++i) { const char* so_name = TARGET_MODULES[i]; __android_log_print(ANDROID_LOG_INFO, kTag, "try to hook function in %s.", so_name); loaded_soinfo* soinfo = elfhook_open(so_name); if (!soinfo) { __android_log_print(ANDROID_LOG_WARN, kTag, "Failure to open %s, try next.", so_name); continue; } //hook OS elfhook_replace(soinfo, "open", (void*)ProxyOpen, (void**)&original_open); elfhook_replace(soinfo, "open64", (void*)ProxyOpen64, (void**)&original_open64); bool is_libjavacore = (strstr(so_name, "libjavacore.so") != nullptr); if (is_libjavacore) { if (!elfhook_replace(soinfo, "read", (void*)ProxyRead, (void**)&original_read)) { __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook read failed, try __read_chk"); if (!elfhook_replace(soinfo, "__read_chk", (void*)ProxyRead, (void**)&original_read)) { __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook failed: __read_chk"); elfhook_close(soinfo); return false; } } //hook OS if (!elfhook_replace(soinfo, "write", (void*)ProxyWrite, (void**)&original_write)) { __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook write failed, try __write_chk"); if (!elfhook_replace(soinfo, "__write_chk", (void*)ProxyWrite, (void**)&original_write)) { __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook failed: __write_chk"); elfhook_close(soinfo); return false; } } } //hook OS elfhook_replace(soinfo, "close", (void*)ProxyClose, (void**)&original_close); elfhook_close(soinfo); } return true; }
hook的替换核心代码:(本质上就是置指针替换)
看看代理方法:
很明显腾讯的人没有考虑到自线程的问题。这里可以优化的。具体的其他部分的细节请参见源码。
Matrix的模块内存泄漏监测核心源码分析
设计目的:
- 自动且较为准确地监测Activity泄漏,发现泄漏之后再触发Dump Hprof而不是根据预先设定的内存占用阈值盲目触发
- 自动获取泄漏的Activity和冗余Bitmap对象的引用链
- 能灵活地扩展Hprof的分析逻辑,必要时允许提取Hprof文件人工分析
为了解决线上监测和后台分析,Matrix的ResourceCanary最终决定将监测步骤和分析步骤拆成两个独立的工具,以满足设计目标。
- Hprof文件留在了服务端,为人工分析提供了机会
- 如果跳过触发Dump Hprof,甚至可以把监测步骤在现网环境启用,以发现测试阶段难以触发的Activity泄漏
客户端解决的问题是内存泄漏的监测和Hprof文件的裁剪,具体看下面的流程图:
ResourcePlugin
ResourcePlugin
是该模块的入口,负责注册Android生命周期的监听以及配置部分参数和接口回调。
ActivityRefWatcherActivityRefWatcher负责的任务有弹出Dump内存的Dialog、Dump内存数据、读取内存数据裁剪Hprof文件、生成包含裁剪后的Hprof以及泄漏的Activity的信息(进程号、Activity名、时间等)、通知主线程完成内存信息的备份并关闭Dialog。
我们看下最为核心的内存泄漏监测代码:
//ActivityRefWatcher private final Application.ActivityLifecycleCallbacks mRemovedActivityMonitor = new ActivityLifeCycleCallbacksAdapter() { private int mAppStatusCounter = 0; private int mUIConfigChangeCounter = 0; @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { mCurrentCreatedActivityCount.incrementAndGet(); } @Override public void onActivityStarted(Activity activity) { if (mAppStatusCounter <= 0) { MatrixLog.i(TAG, "we are in foreground, start watcher task."); mDetectExecutor.executeInBackground(mScanDestroyedActivitiesTask); } if (mUIConfigChangeCounter < 0) { ++mUIConfigChangeCounter; } else { ++mAppStatusCounter; } } @Override public void onActivityStopped(Activity activity) { if (activity.isChangingConfigurations()) { --mUIConfigChangeCounter; } else { --mAppStatusCounter; if (mAppStatusCounter <= 0) { MatrixLog.i(TAG, "we are in background, stop watcher task."); mDetectExecutor.clearTasks(); } } } @Override public void onActivityDestroyed(Activity activity) { //当activity销毁的时候开始。。。 pushDestroyedActivityInfo(activity); synchronized (mDestroyedActivityInfos) { mDestroyedActivityInfos.notifyAll(); } } };
private void pushDestroyedActivityInfo(Activity activity) { final String activityName = activity.getClass().getName(); //该Activity确认存在泄漏,且已经上报 if (isPublished(activityName)) { MatrixLog.d(TAG, "activity leak with name %s had published, just ignore", activityName); return; } final UUID uuid = UUID.randomUUID(); final StringBuilder keyBuilder = new StringBuilder(); //生成Activity实例的唯一标识 keyBuilder.append(ACTIVITY_REFKEY_PREFIX).append(activityName) .append('_').append(Long.toHexString(uuid.getMostSignificantBits())).append(Long.toHexString(uuid.getLeastSignificantBits())); final String key = keyBuilder.toString(); //构造一个数据结构,表示一个已被destroy的Activity final DestroyedActivityInfo destroyedActivityInfo = new DestroyedActivityInfo(key, activity, activityName, mCurrentCreatedActivityCount.get()); //放入ConcurrentLinkedQueue数据结构中,用于后续的检查 mDestroyedActivityInfos.add(destroyedActivityInfo); }
内存泄漏的核心代码:
private final RetryableTask mScanDestroyedActivitiesTask = new RetryableTask() { @Override public Status execute() { // If destroyed activity list is empty, just wait to save power. while (mDestroyedActivityInfos.isEmpty()) { synchronized (mDestroyedActivityInfos) { try { mDestroyedActivityInfos.wait(); } catch (Throwable ignored) { // Ignored. } } } // Fake leaks will be generated when debugger is attached. //Debug调试模式,检测可能失效,直接return if (Debug.isDebuggerConnected() && !mResourcePlugin.getConfig().getDetectDebugger()) { MatrixLog.w(TAG, "debugger is connected, to avoid fake result, detection was delayed."); return Status.RETRY; } //创建一个对象的弱引用 final WeakReference<Object> sentinelRef = new WeakReference<>(new Object()); triggerGc(); //系统未执行GC,直接return if (sentinelRef.get() != null) { // System ignored our gc request, we will retry later. MatrixLog.d(TAG, "system ignore our gc request, wait for next detection."); return Status.RETRY; } final Iterator<DestroyedActivityInfo> infoIt = mDestroyedActivityInfos.iterator(); while (infoIt.hasNext()) { final DestroyedActivityInfo destroyedActivityInfo = infoIt.next(); //该实例对应的Activity已被标泄漏,跳过该实例 if (isPublished(destroyedActivityInfo.mActivityName)) { MatrixLog.v(TAG, "activity with key [%s] was already published.", destroyedActivityInfo.mActivityName); infoIt.remove(); continue; } //若不能通过弱引用获取到Activity实例,表示已被回收,跳过该实例 if (destroyedActivityInfo.mActivityRef.get() == null) { // The activity was recycled by a gc triggered outside. MatrixLog.v(TAG, "activity with key [%s] was already recycled.", destroyedActivityInfo.mKey); infoIt.remove(); continue; } //该Activity实例 检测到泄漏的次数+1 ++destroyedActivityInfo.mDetectedCount; //当前显示的Activity实例与泄漏的Activity实例相差几个Activity跳转 long createdActivityCountFromDestroy = mCurrentCreatedActivityCount.get() - destroyedActivityInfo.mLastCreatedActivityCount; //若Activity实例 检测到泄漏的次数未达到阈值,或者泄漏的Activity与当前显示的Activity很靠近,可认为是一种容错手段(实际应用中有这种场景),跳过该实例 if (destroyedActivityInfo.mDetectedCount < mMaxRedetectTimes || (createdActivityCountFromDestroy < CREATED_ACTIVITY_COUNT_THRESHOLD && !mResourcePlugin.getConfig().getDetectDebugger())) { // Although the sentinel tell us the activity should have been recycled, // system may still ignore it, so try again until we reach max retry times. MatrixLog.i(TAG, "activity with key [%s] should be recycled but actually still \n" + "exists in %s times detection with %s created activities during destroy, wait for next detection to confirm.", destroyedActivityInfo.mKey, destroyedActivityInfo.mDetectedCount, createdActivityCountFromDestroy); continue; } MatrixLog.i(TAG, "activity with key [%s] was suspected to be a leaked instance.", destroyedActivityInfo.mKey); if (mHeapDumper != null) { final File hprofFile = mHeapDumper.dumpHeap(); if (hprofFile != null) { markPublished(destroyedActivityInfo.mActivityName); final HeapDump heapDump = new HeapDump(hprofFile, destroyedActivityInfo.mKey, destroyedActivityInfo.mActivityName); mHeapDumpHandler.process(heapDump); infoIt.remove(); } else { MatrixLog.i(TAG, "heap dump for further analyzing activity with key [%s] was failed, just ignore.", destroyedActivityInfo.mKey); infoIt.remove(); } } else { // Lightweight mode, just report leaked activity name. MatrixLog.i(TAG, "lightweight mode, just report leaked activity name."); markPublished(destroyedActivityInfo.mActivityName); if (mResourcePlugin != null) { final JSONObject resultJson = new JSONObject(); try { resultJson.put(SharePluginInfo.ISSUE_ACTIVITY_NAME, destroyedActivityInfo.mActivityName); } catch (JSONException e) { MatrixLog.printErrStackTrace(TAG, e, "unexpected exception."); } mResourcePlugin.onDetectIssue(new Issue(resultJson)); } } } return Status.RETRY; } };
RetryableTaskExecutor
RetryableTaskExecutor
中包含了两个Handler对象,一个mBackgroundHandler
和mMainHandler
,分别给主线程和后台的线程提交任务。默认重试次数是3。
AndroidHeapDumper
AndroidHeapDumper
这个其实就是封装了android.os.Debug
的接口的类。主要是用系统提供的类android.os.Debug
Dump内存信息到本地,android.os.Debug
会在本地生成一个Hprof文件,也是Matrix需要分析和裁剪的原始文件。
注意:一般Dump一次要5s~15s之间,线上建议不要使用,有一定的风险。
Dump的时候,AndroidHeapDumper
会展示一个Dialog提示当前正在Dump中,Dump完毕就会将Dialog关闭。
Debug.dumpHprofData(hprofFile.getAbsolutePath());
Matrix的模块Trace模块核心源码分析
Trace Canary: 用于监控界面流畅性、启动耗时、页面切换耗时、慢函数及卡顿等问题。(思考一下,技术上怎么实现)
入口函数探针分析:
public class TracePlugin extends Plugin { private static final String TAG = "Matrix.TracePlugin"; private final TraceConfig traceConfig; private EvilMethodTracer evilMethodTracer;//慢函数 private StartupTracer startupTracer; //启动监测 private FrameTracer frameTracer; //fps private AnrTracer anrTracer; //anr public TracePlugin(TraceConfig config) { this.traceConfig = config; } ...
【关键知识点1】: MessageQueue中的IdleHandler接口有什么用?
在Android中,我们可以处理Message,这个Message我们可以立即执行也可以delay 一定时间执行。Handler线程在执行完所有的Message消息,它会wait,进行阻塞,直到有新的Message到达。如果这样子,那么这个线程也太浪费了。MessageQueue提供了另一类消息,IdleHandler。也就是说当我们的MessageQueue中的消息被处理完后,就会触发一次或者多次回调消息。
应用场景:1、比如主线程在开始加载页面完成后,如果线程空闲就提前加载些二级页面的内容。
2、消息触发器 例如在APM中的作用
3、优化Activity的启动时间,在Resume中是不是可以增加idle的监听
Looper.myQueue().addIdleHandler(() -> { initializeData(); return false; });
源码分析:
Message next(){ // Return here if the message loop has already quit and been disposed. // This can happen if the application tries to restart a looper after quit // which is not supported. final long ptr = mPtr; if (ptr == 0) { return null; } int pendingIdleHandlerCount = -1; // -1 only during first iteration int nextPollTimeoutMillis = 0; for (;;) { if (nextPollTimeoutMillis != 0) { Binder.flushPendingCommands(); } nativePollOnce(ptr, nextPollTimeoutMillis); synchronized (this) { // Try to retrieve the next message. Return if found. final long now = SystemClock.uptimeMillis(); Message prevMsg = null; Message msg = mMessages; if (msg != null && msg.target == null) { // Stalled by a barrier. Find the next asynchronous message in the queue. do { prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous()); } if (msg != null) { if (now < msg.when) { // Next message is not ready. Set a timeout to wake up when it is ready. nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); } else { // Got a message. mBlocked = false; if (prevMsg != null) { prevMsg.next = msg.next; } else { mMessages = msg.next; } msg.next = null; if (DEBUG) Log.v(TAG, "Returning message: " + msg); msg.markInUse(); return msg; } } else { // No more messages. nextPollTimeoutMillis = -1; } // Process the quit message now that all pending messages have been handled. if (mQuitting) { dispose(); return null; } // If first time idle, then get the number of idlers to run. // Idle handles only run if the queue is empty or if the first message // in the queue (possibly a barrier) is due to be handled in the future. if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) { pendingIdleHandlerCount = mIdleHandlers.size(); } if (pendingIdleHandlerCount <= 0) { // No idle handlers to run. Loop and wait some more. mBlocked = true; continue; } if (mPendingIdleHandlers == null) { mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)]; } mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); } // Run the idle handlers. // We only ever reach this code block during the first iteration. for (int i = 0; i < pendingIdleHandlerCount; i++) { final IdleHandler idler = mPendingIdleHandlers[i]; mPendingIdleHandlers[i] = null; // release the reference to the handler boolean keep = false; try { keep = idler.queueIdle(); } catch (Throwable t) { Log.wtf(TAG, "IdleHandler threw exception", t); } //根据IdleHandler中的回掉方法来判断是否移除 if (!keep) { synchronized (this) { mIdleHandlers.remove(idler); } } } // Reset the idle handler count to 0 so we do not run them again. pendingIdleHandlerCount = 0; // While calling an idle handler, a new message could have been delivered // so go back and look again for a pending message without waiting. nextPollTimeoutMillis = 0; } }
LooperMonitor类监测卡顿问题:
发生在Android主线程的每16ms重绘操作依赖于Main Looper中消息的发送和获取。如果App一切运行正常,无卡顿无丢帧现象发生,那么开发者的代码在主线程Looper消息队列中发送和接收消息的时间会很短,理想情况是16ms,这是也是Android系统规定的时间。但是,如果一些发生在主线程的代码写的太重,执行任务花费时间太久,就会在主线程延迟Main Looper的消息在16ms尺度范围内的读和写。
我们如何检测卡顿的问题?
使用主线程的Looper监测系统发生的卡顿和丢帧。编程技巧是设置一个阈值,看是否可以打印stack信息。
网络上说使用Android的Choreographer监测App发生的UI卡顿丢帧问题,本质上还是利用了Android的主线程的Looper消息机制。Android 系统每隔 16.67 ms 都会发送一个 VSYNC 信号触发 UI 的渲染,正常情况下两个 VSYNC 信号之间是 16.67 ms ,如果超过 16.67 ms 则可以认为渲染发生了卡顿。
Choreographer.getInstance() .postFrameCallback(new Choreographer.FrameCallback() { @Override public void doFrame(long l) { if(frameTimeNanos - mLastFrameNanos > 100) { ... } mLastFrameNanos = frameTimeNanos; Choreographer.getInstance().postFrameCallback(this); } });
本质:判断相邻的两次 FrameCallback.doFrame(long l)
间隔是否超过阈值,如果超过阈值则发生了卡顿,则可以在另外一个子线程中 dump 当前主线程的堆栈信息进行分析。
消息处理
UIThreadMonitor类
init():
public void init(TraceConfig config) { if (Thread.currentThread() != Looper.getMainLooper().getThread()) { throw new AssertionError("must be init in main thread!"); } this.isInit = true; this.config = config; choreographer = Choreographer.getInstance(); callbackQueueLock = reflectObject(choreographer, "mLock"); callbackQueues = reflectObject(choreographer, "mCallbackQueues"); // 代码 1 addInputQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_INPUT], ADD_CALLBACK, long.class, Object.class, Object.class); // 代码 2 addAnimationQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_ANIMATION], ADD_CALLBACK, long.class, Object.class, Object.class); // 代码 3 addTraversalQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_TRAVERSAL], ADD_CALLBACK, long.class, Object.class, Object.class); // 代码 4 frameIntervalNanos = reflectObject(choreographer, "mFrameIntervalNanos"); LooperMonitor.register(new LooperMonitor.LooperDispatchListener() { // 代码 5 @Override public boolean isValid() { return isAlive; } @Override public void dispatchStart() { super.dispatchStart(); UIThreadMonitor.this.dispatchBegin(); // 代码 6 } @Override public void dispatchEnd() { super.dispatchEnd(); UIThreadMonitor.this.dispatchEnd(); // 代码 7 } }); ...... }
代码 1:
- 通过反射拿到了 Choreographer 实例的 mCallbackQueues 属性,mCallbackQueues 是一个回调队列数组 CallbackQueue[] mCallbackQueues,其中包括四个回调队列,
- 第一个是输入事件回调队列 CALLBACK_INPUT = 0,
- 第二个是动画回调队列 CALLBACK_ANIMATION = 1,
- 第三个是遍历绘制回调队列 CALLBACK_TRAVERSAL = 2,
- 第四个是提交回调队列 CALLBACK_COMMIT = 3。 这四个阶段在每一帧的 UI 渲染中是依次执行的,每一帧中各个阶段开始时都会回调 mCallbackQueues 中对应的回调队列的回调方法。
代码 2:
- 通过反射拿到输入事件回调队列的 addCallbackLocked 方法
代码 3:
- 通过反射拿到动画回调队列的 addCallbackLocked 方法
代码 4:
- 通过反射拿到遍历绘制回调队列的addCallbackLocked 方法
代码 5:
- 通过 LooperMonitor.register(LooperDispatchListener listener) 方法向 LooperMonitor 中设置 LooperDispatchListener listener
- 代码 6:
- 在 Looper.loop() 中的消息处理开始时的回调
- 代码 7:
- 在 Looper.loop() 中的消息处理结束时的回调
核心:
private void dispatchBegin() { //记录2个时间 线程起始时间 和CPU的开始时间 token = dispatchTimeMs[0] = SystemClock.uptimeMillis(); dispatchTimeMs[2] = SystemClock.currentThreadTimeMillis(); AppMethodBeat.i(AppMethodBeat.METHOD_ID_DISPATCH); synchronized (observers) { for (LooperObserver observer : observers) { if (!observer.isDispatchBegin()) { observer.dispatchBegin(dispatchTimeMs[0], dispatchTimeMs[2], token); } } } }
private void dispatchEnd() { // 代码 3 if (isBelongFrame) { doFrameEnd(token); } dispatchTimeMs[3] = SystemClock.currentThreadTimeMillis(); dispatchTimeMs[1] = SystemClock.uptimeMillis(); AppMethodBeat.o(AppMethodBeat.METHOD_ID_DISPATCH); synchronized (observers) { for (LooperObserver observer : observers) { if (observer.isDispatchBegin()) { observer.dispatchEnd(dispatchTimeMs[0], dispatchTimeMs[2], dispatchTimeMs[1], dispatchTimeMs[3], token, isBelongFrame); } } } }
核心点总结:
- 在 UIThreadMonitor 中有两个长度是三的数组
queueStatus
和queueCost
分别对应着每一帧中输入事件阶段、动画阶段、遍历绘制阶段的状态和耗时,queueStatus
有三个值:DO_QUEUE_DEFAULT、DO_QUEUE_BEGIN 和 DO_QUEUE_END。
UIThreadMonitor
实现Runnable
接口,也是为了将UIThreadMonitor
作为输入事件回调CALLBACK_INPUT
的回调方法,设置到Choreographer
中去的。
看到这里应该搞明白了卡顿的检测原理,那么FPS的计算呢?
每一帧的时间信息通过 HashSet observers
回调出去,看一下是在哪里向 observers
添加 LooperObserver
回调的。主要看一下 FrameTracer
这个类,其中涉及到了帧率 FPS 的计算相关的代码。
FPSCollector
是 FrameTracer
的一个内部类,实现了 IDoFrameListener
接口,主要逻辑是在 doFrameAsync()
方法中
- 代码 1:会根据当前 ActivityName 创建一个对应的 FrameCollectItem 对象,存放在 HashMap 中
- 代码 2:调用
FrameCollectItem#collect()
,计算帧率 FPS 等一些信息 - 代码 3:如果此 Activity 的绘制总时间超过 timeSliceMs(默认是 10s),则调用
FrameCollectItem#report()
上报统计数据,并从 HashMap 中移除当前 ActivityName 和对应的 FrameCollectItem 对象
private class FPSCollector extends IDoFrameListener { private Handler frameHandler = new Handler(MatrixHandlerThread.getDefaultHandlerThread().getLooper()); private HashMap<String, FrameCollectItem> map = new HashMap<>(); @Override public Handler getHandler() { return frameHandler; } @Override public void doFrameAsync(String focusedActivityName, long frameCost, int droppedFrames) { super.doFrameAsync(focusedActivityName, frameCost, droppedFrames); if (Utils.isEmpty(focusedActivityName)) { return; } FrameCollectItem item = map.get(focusedActivityName); // 代码 1 if (null == item) { item = new FrameCollectItem(focusedActivityName); map.put(focusedActivityName, item); } item.collect(droppedFrames); // 代码 2 if (item.sumFrameCost >= timeSliceMs) { // report // 代码 3 map.remove(focusedActivityName); item.report(); } } }
FrameCollectItem:计算FPS。
- 根据
float fps = Math.min(60.f, 1000.f * sumFrame / sumFrameCost)
计算 fps 值 - sumDroppedFrames 统计总的掉帧个数
- sumFrameCost 代表总耗时
计算帧率参考资料:
github.com/friendlyrob… 熟读代码 并应用到自己的项目中。
关于hook的部分:
利用了反射机制进行了hook,代码比较清晰,目的很明确就是利用自己写的HackCallback来替换ActivityThread类里的mCallback,达到偷梁换柱的效果,这样做的意义就是可以拦截mCallback的原有的消息,然后选择自己要处理的消息。搞清楚Activity的启动流程 AMS和ActivityThread进程之间的交互。
获取到Activity的启动时机。
AppMethodBeat 通过hook记录每个方法执行的时间。
/** * hook method when it's called in. * * @param methodId */ public static void i(int methodId) { if (status <= STATUS_STOPPED) { return; } if (methodId >= METHOD_ID_MAX) { return; } if (status == STATUS_DEFAULT) { synchronized (statusLock) { if (status == STATUS_DEFAULT) { realExecute(); status = STATUS_READY; } } } if (Thread.currentThread().getId() == sMainThread.getId()) { if (assertIn) { android.util.Log.e(TAG, "ERROR!!! AppMethodBeat.i Recursive calls!!!"); return; } assertIn = true; if (sIndex < Constants.BUFFER_SIZE) { mergeData(methodId, sIndex, true); } else { sIndex = -1; } ++sIndex; assertIn = false; } } /** * hook method when it's called out. * * @param methodId */ public static void o(int methodId) { if (status <= STATUS_STOPPED) { return; } if (methodId >= METHOD_ID_MAX) { return; } if (Thread.currentThread().getId() == sMainThread.getId()) { if (sIndex < Constants.BUFFER_SIZE) { mergeData(methodId, sIndex, false); } else { sIndex = -1; } ++sIndex; } } /** * merge trace info as a long data * * @param methodId * @param index * @param isIn */ private static void mergeData(int methodId, int index, boolean isIn) { if (methodId == AppMethodBeat.METHOD_ID_DISPATCH) { //记录上面2个方法的时间差 sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime; } long trueId = 0L; if (isIn) { trueId |= 1L << 63; } trueId |= (long) methodId << 43; trueId |= sCurrentDiffTime & 0x7FFFFFFFFFFL; sBuffer[index] = trueId; checkPileup(index); sLastIndex = index; }
源码读到这里是不是可以想一想,我们应该怎么找方法的调用?
利用ASM技术完成在方法前执行“com/tencent/matrix/trace/core/AppMethodBeat”这个class里的i()
方法,在每个方法最后执行o()
方法。
ASM
应用场景:
- 无痕埋点
- hook
- apm监控
- 编译期间动态修改代码需求
- ASM是什么
ASM 可以直接产生二进制的class 文件,也可以在增强既有类的功能。Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。
blog.jamesdbloom.com/JavaCodeToB…
class 文件结构:
- Magic:该项存放了一个 Java 类文件的魔数(magic number)和版本信息。一个 Java 类文件的前 4 个字节被称为它的魔数。每个正确的 Java 类文件都是以 0xCAFEBABE 开头的,这样保证了 Java 虚拟机能很轻松的分辨出 Java 文件和非 Java 文件。
- **Version:**该项存放了 Java 类文件的版本信息,它对于一个 Java 文件具有重要的意义。因为 Java 技术一直在发展,所以类文件的格式也处在不断变化之中。类文件的版本信息让虚拟机知道如何去读取并处理该类文件。
- **Constant Pool:**该项存放了类中各种文字字符串、类名、方法名和接口名称、final 变量以及对外部类的引用信息等常量。虚拟机必须为每一个被装载的类维护一个常量池,常量池中存储了相应类型所用到的所有类型、字段和方法的符号引用,因此它在 Java 的动态链接中起到了核心的作用。常量池的大小平均占到了整个类大小的 60% 左右。
- Access_flag: 该项指明了该文件中定义的是类还是接口(一个 class 文件中只能有一个类或接口),同时还指名了类或接口的访问标志,如 public,private, abstract 等信息。ACC_
- This Class: 指向表示该类全限定名称的字符串常量的指针。
- Super Class: 指向表示父类全限定名称的字符串常量的指针。
- Interfaces: 一个指针数组,存放了该类或父类实现的所有接口名称的字符串常量的指针。以上三项所指向的常量,特别是前两项,在我们用 ASM 从已有类派生新类时一般需要修改:将类名称改为子类名称;将父类改为派生前的类名称;如果有必要,增加新的实现接口。
- Fields: 该项对类或接口中声明的字段进行了细致的描述。需要注意的是,fields 列表中仅列出了本类或接口中的字段,并不包括从超类和父接口继承而来的字段。
- Methods: 该项对类或接口中声明的方法进行了细致的描述。例如方法的名称、参数和返回值类型等。需要注意的是,methods 列表里仅存放了本类或本接口中的方法,并不包括从超类和父接口继承而来的方法。使用 ASM 进行 AOP 编程,通常是通过调整 Method 中的指令来实现的。
- Class attributes: 该项存放了在该文件中类或接口所定义的属性的基本信息。
核心类
- ClassReader:该类用来解析编译过的class字节码文件。(事件生产者)
- ClassWriter:该类用来重新构建编译后的类,比如说修改类名、属性以及方法,甚至可以生成新的类的字节码文件。(事件消费者,是ClassVisitor的子类)
- ClassVisitor:主要负责 “拜访” 类成员信息。其中包括标记在类上的注解,类的构造方法,类的字段,类的方法,静态代码块。
- AdviceAdapter:实现了MethodVisitor接口,主要负责 “拜访” 方法的信息,用来进行具体的方法字节码操作。
函数插桩
什么是函数插桩?
插桩:目标程序代码中某些位置插入或修改成一些代码,从而在目标程序运行过程中获取某些程序状态并加以分析。简单来说就是在代码中插入代码。 那么函数插桩,便是在函数中插入或修改代码。在Android编译过程中,往字节码里插入自定义的字节码,所以也可以称为字节码插桩。
作用
函数插桩可以帮助我们实现很多手术刀式的代码设计,如无埋点统计上报、轻量级AOP等。 应用到在Android中,可以用来做用行为统计、方法耗时统计等功能。
About Gradle Transform API
Transform的使用场景:
一般我们使用Transform
会有下面两种场景
- 我们需要对编译class文件做自定义的处理。
- 我们需要读取编译产生的class文件,做一些其他事情,但是不需要修改它。
public abstract class Transform { public abstract String getName(); //自定义的Transform的名字 public abstract Set<ContentType> getInputTypes(); //Transform处理的输入类型 (CLASSES、RESOURCES) public abstract Set<? super Scope> getScopes(); public abstract boolean isIncremental(); // 是否支持增量编译 }
Scope代码:
enum Scope implements ScopeType { /** Only the project content */ PROJECT(0x01), //只是当前工程的代码 /** Only the project's local dependencies (local jars) */ PROJECT_LOCAL_DEPS(0x02), // 工程的本地jar /** Only the sub-projects. */ SUB_PROJECTS(0x04), // 只包含子工工程 /** Only the sub-projects's local dependencies (local jars). */ SUB_PROJECTS_LOCAL_DEPS(0x08), /** Only the external libraries */ EXTERNAL_LIBRARIES(0x10), /** Code that is being tested by the current variant, including dependencies */ TESTED_CODE(0x20), /** Local or remote dependencies that are provided-only */ PROVIDED_ONLY(0x40); }
测试代码:
public void transform(TransformInvocation invocation) { for (TransformInput input : invocation.getInputs()) { input.getJarInputs().parallelStream().forEach(jarInput -> { File src = jarInput.getFile(); JarFile jarFile = new JarFile(file); Enumeration<JarEntry> entries = jarFile.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); //处理 } } }