Matrix是微信开源的一套完整的APM解决方案,内部包含Resource Canary(资源监测)/Trace Canary(卡顿监测)/IO Canary(IO监测)等。android
本篇为卡顿分析系列文章之二,分析Trace Canary相关的原理,基于版本0.5.2.43。文章有点长,建议你先大体浏览一遍再细看,对你必定有帮助。第一篇传送门Android卡顿检测工具(一)BlockCanary。git
可见Matrix做为一个APM工具,在性能检测方面仍是很是全面的,系列文章将会一一对它们进行分析。github
为理清源代码结构咱们先从初始化流程讲起,项目地址Matrix。json
Matrix.Builder内部类配置Plugins。数组
//建立builder
Matrix.Builder builder = new Matrix.Builder(this);
//可选 感知插件状态变化,onReportIssue获取/处理issue
builder.patchListener(...);
//可选 配置插件
builder.plugin(tracePlugin);
builder.plugin(ioCanaryPlugin);
//完成初始化
Matrix.init(builder.build());
复制代码
目前配置的pluginbash
本篇分析的是TracePlugin,它与卡顿/UI渲染效率相关。微信
Matrix.Builder调用build方法触发Matrix构造函数。app
private Matrix(Application app, PluginListener listener, HashSet<Plugin> plugins) {
this.application = app;
this.pluginListener = listener;
this.plugins = plugins;
//(1)
AppActiveMatrixDelegate.INSTANCE.init(application);
for (Plugin plugin : plugins) {
//(2)
plugin.init(application, pluginListener);
pluginListener.onInit(plugin);
}
}
复制代码
AppActiveMatrixDelegate是一个枚举类(使人费解,枚举性能并很差,此类做用跟普通的单例类同样),在其init方法中为application注册了ActivityLifecycle和ComponentCallbacks2监听,可见它是为了拿到应用内部全部Activity生命周期状态和内存紧缺状态(onTrimMemory/onLowMemory)以供后续使用。框架
内部遍历全部插件,并调用其init方法进行初始化,以后通知pluginListener生命周期方法onInit。异步
PluginListener包含的生命周期以下:
# -> PluginListener
public interface PluginListener {
//初始化
void onInit(Plugin plugin);
//插件开始运行
void onStart(Plugin plugin);
//插件中止运行
void onStop(Plugin plugin);
//插件销毁
void onDestroy(Plugin plugin);
//插件捕捉到Issue,包括卡顿、ANR等等
void onReportIssue(Issue issue);
}
复制代码
通常来讲上层须要自定义一个的PluginListener,由于onReportIssue方法是具体处理Issue的关键方法,官方sample的作法是收到issue时弹出一个IssuesListActivity展现issue具体信息,而Matrix框架定义的DefaultPluginListener什么都没作。做为接入方咱们可能会作更丰富的处理,好比序列化到本地、上传云端等等,全部的这一切都要从自定义PluginListener并实现onReportIssue方法开始。
patchListener方法简单的为成员变量赋值。
# -> Matrix.Builder
public Builder patchListener(PluginListener pluginListener) {
this.pluginListener = pluginListener;
return this;
}
复制代码
最终来看Matrix的init方法,其实就是为其静态成员变量sInstance赋值。
# -> Matrix
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;
}
复制代码
能够看到Matrix提供了日志管理器MatrixLogImpl,以及操做其内部全部plugin的各类方法。
接下来进入正题,咱们来看看卡顿(UI渲染性能)分析模块TracePlugin是如何工做的。
它是tracer管理器,其内部定义了四个跟踪器。
来看一下类图:
这些跟踪器都继承于Tracer,它是一个抽象类,但不含抽象方法,已对继承来的接口都作了默认实现。
为了了解这些Tracer能实现哪些功能,咱们先来看看Tracer继承父类和实现的接口。
它是一个抽象类,内部定义了三个重要方法dispatchBegin/doFrame/dispatchEnd,但只是空实现,这三个方法都跟监听主线程Handler的消息处理有关。当主线程处理一条消息前会回调dispatchBegin,消息处理完会先调用doFrame,而后再调用dispatchEnd。之因此这么作是由于对于卡顿的检测一般有两种方式。
第一种方式是经过hook Looper内部的logger对象实现的。系统Looper分发处理消息先后会经过logger对象打印日志,hook这个logger至关于拿到了一条消息的先后时间点,根据两者的时间差能够作不少卡顿的分析,BlockCanary就是用此方法实现卡顿检测,具体参看Android卡顿检测工具(一)BlockCanary
第二种方式是Choreographer开放API,上层可设置FrameCallback监听,从而得到每一帧绘制完毕的onFrame回调。经常使用的帧率监测工具(FPS)就是经过分析两帧以前的时间差完成FPS的计算,好比TinyDancer、Takt。
实际上Matrix早期版本用的是第二种方式,最新版使用了第一种方式,由于能够拿到更完整更清晰的堆栈信息。
至此,咱们能够推断Tracer具备感知帧率变化、统计卡顿的能力,因此跟帧率、函数耗时统计相关的Tracer(FrameTracer/EvilMethodTracer/AnrTracer)必定会继续复写doFrame方法,以实现具体功能。
它是一个接口,继承了IAppForeground接口,整体算下来一共四个抽象方法:onStartTrace、onCloseTrace、isAlive、onForeground。前三个方法是在描述Tracer自身的生命周期,由TracePlugin统一管理。当Activity先后台状态发生变化时回调Tracer的onForeground方法,所以Tracer具备感知Activity先后台状态变化的能力,它可用来作启动分析。
在Tracer中大部分接口方法都是空实现,具体实现交由有需求的tracer完成。下面咱们来看TraceCanary包含的具体tracer实现。
咱们先来看FrameTracer,它复写doFrame监听每一帧的回调,并将时间戳、掉帧状况、页面名称等信息发送给IDoFrameListener。
# -> FrameTracer -> doFrame
@Override
public void doFrame(final long lastFrameNanos, final long frameNanos) {
if (!isDrawing) {
return;
}
isDrawing = false;
final int droppedCount = (int) ((frameNanos - lastFrameNanos) / REFRESH_RATE_MS);
for (final IDoFrameListener listener : mDoFrameListenerList) {
//同步发送
listener.doFrameSync(lastFrameNanos, frameNanos, getScene(), droppedCount);
if (null != listener.getHandler()) {
//异步发送
listener.getHandler().post(new AsyncDoFrameTask(listener,
lastFrameNanos, frameNanos, getScene(), droppedCount));
}
}
}
复制代码
能够看到代码中分别以同步和异步的方式将回调发送出去,上层可经过FrameTracer的register方法注册监听。
# FrameTracer
public void register(IDoFrameListener listener) {
if (FrameBeat.getInstance().isPause()) {
FrameBeat.getInstance().resume();
}
if (!mDoFrameListenerList.contains(listener)) {
mDoFrameListenerList.add(listener);
}
}
public void unregister(IDoFrameListener listener) {
mDoFrameListenerList.remove(listener);
if (!FrameBeat.getInstance().isPause() && mDoFrameListenerList.isEmpty()) {
FrameBeat.getInstance().removeListener(this);
}
}
复制代码
它具备检查耗时函数的功能,而ANR就是最严重的耗时状况,那咱们先来看看ANR检查是如何作到的。
先来看构造器
public EvilMethodTracer(TracePlugin plugin, TraceConfig config) {
super(plugin);
this.mTraceConfig = config;
//建立ANR延时检测工具 定时5s
mLazyScheduler = new LazyScheduler(MatrixHandlerThread.getDefaultHandlerThread(), Constants.DEFAULT_ANR);
mActivityCreatedInfoMap = new HashMap<>();
}
复制代码
LazyScheduler是一个延时任务工具类,构造时需设定HandlerThread和delay。
内部ILazyTask接口定义了延时任务执行时的回调方法onTimeExpire。setUp方法开始埋炸弹(ANR和耗时方法),cancel方法解除炸弹。也就是说调用setUp方法后5秒内若是没有执行cancel,就会触发onTimeExpire方法。
上面的内容理解以后,咱们来看doFrame方法。
# -> EvilMethodTracer
@Override
public void doFrame(long lastFrameNanos, long frameNanos) {
if (isIgnoreFrame) {
mActivityCreatedInfoMap.clear();
setIgnoreFrame(false);
getMethodBeat().resetIndex();
return;
}
int index = getMethodBeat().getCurIndex();
//两帧时间差大于卡顿阈值(默认一秒)则发出buffer信息
//若知足一系列校验工做则触发卡顿检测
if (hasEntered && frameNanos - lastFrameNanos > mTraceConfig.getEvilThresholdNano()) {
MatrixLog.e(TAG, "[doFrame] dropped frame too much! lastIndex:%s index:%s", 0, index);
handleBuffer(Type.NORMAL, 0, index - 1, getMethodBeat().getBuffer(), (frameNanos - lastFrameNanos) / Constants.TIME_MILLIS_TO_NANO);
}
getMethodBeat().resetIndex();
mLazyScheduler.cancel();
//埋ANR炸弹
mLazyScheduler.setUp(this, false);
}
复制代码
若是5秒内还没执行下一次doFrame,就会回调到EvilMethodTracer的onTimeExpire方法。
# -> EvilMethodTracer
@Override
public void onTimeExpire() {
// maybe ANR
if (isBackground()) {
MatrixLog.w(TAG, "[onTimeExpire] pass this time, on Background!");
return;
}
long happenedAnrTime = getMethodBeat().getCurrentDiffTime();
MatrixLog.w(TAG, "[onTimeExpire] maybe ANR!");
setIgnoreFrame(true);
getMethodBeat().lockBuffer(false);
//处于前台就会发送ANR消息
handleBuffer(Type.ANR, 0, getMethodBeat().getCurIndex() - 1, getMethodBeat().getBuffer(), null, Constants.DEFAULT_ANR, happenedAnrTime, -1);
}
复制代码
对于普通耗时函数又是如何检测的呢?EvilMethodTracer的工做流程是这样的:
MethodTracer的内部类TraceMethodAdapter负责为每一个方法执行前插入MethodBeat的i方法,方法执行后插入o方法。插桩使用的是ASM实现的,ASM是一种经常使用的操做字节码的动态化技术,能够用作无侵入的埋点统计。EvilMethodTracer也是用它作耗时函数的分析。
# -> MethodTracer.TraceMethodAdapter
@Override
protected void onMethodEnter() {
TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
if (traceMethod != null) {
traceMethodCount.incrementAndGet();
mv.visitLdcInsn(traceMethod.id);
//入口插桩
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "i", "(I)V", false);
}
}
@Override
protected void onMethodExit(int opcode) {
TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
if (traceMethod != null) {
if (hasWindowFocusMethod && mTraceConfig.isActivityOrSubClass(className, mCollectedClassExtendMap)
&& mCollectedMethodMap.containsKey(traceMethod.getMethodName())) {
TraceMethod windowFocusChangeMethod = TraceMethod.create(-1, Opcodes.ACC_PUBLIC, className,
TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD, TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD_ARGS);
if (windowFocusChangeMethod.equals(traceMethod)) {
traceWindowFocusChangeMethod(mv);
}
}
traceMethodCount.incrementAndGet();
mv.visitLdcInsn(traceMethod.id);
//出口插桩
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o", "(I)V", false);
}
}
复制代码
Matrix经过代理编译期间的任务 transformClassesWithDexTask,将全局 class 文件做为输入,利用 ASM 工具,高效地对全部 class 文件进行扫描及插桩。为了尽量的下降性能损耗扫描过程会过滤掉一些默认或匿名的构造函数以及get/set等简单而不耗时的函数。
为了方便及高效记录函数执行过程,Matrix插件为每一个插桩的函数分配一个独立 ID,在插桩过程当中,记录插桩的函数签名及分配的 ID,在插桩完成后输出一份 methodmap文件,做为数据上报后的解析支持,该文件在apk构建时生成,目录位于build/matrix_output下,名为Debug_methodmap(debug构建),而那些被过滤掉的方法被记录在Debug_ignoremethodmap文件中。文件生成规则在MethodCollector类中,感兴趣的小伙伴能够继续研究。
那接下来咱们来看一下生成文件的内容。
文件每一行表明一个插桩方法。 以第一行为例:
-1,1,sample.tencent.matrix.io.TestIOActivity onWindowFocusChanged (Z)V
复制代码
接下来咱们来看一下实践是什么效果,咱们模拟了一个耗时函数,当点击按钮时调用。
//点击按钮触发 为放大耗时,循环执行200次
public void testJank(View view) {
for (int i = 0; i < 200; i++) {
wrapper();
}
}
//包装方法用于测试调用深度
void wrapper() {
tryHeavyMethod();
}
//dump内存是耗时方法
private void tryHeavyMethod() {
Debug.getMemoryInfo(new Debug.MemoryInfo());
}
复制代码
运行后获得如下Issue:
咱们重点关心的是
例子中stack(0,28,1,1988\n 1,31,1,136)如何解读呢?四个数为一组每组用换行符分隔,其中一组四个数分别表示为:
咱们经过反查methodmap函数可验证结果。
实测发现stack存在bug,咱们的代码中最终的耗时方法是tryHeavyMethod,只不过中间包了一层wrapper方法,stack就不能识别到了。这一点Matrix官方可能会后续修复吧。
stackKey就是耗时函数的入口。本例中testJank调用wrapper,wrapper调用tryHeavyMethod,统计stackKey时以深度为0的函数为准,28就对应testJank方法。
同其余相似的fps检测工具原理同样,监听Choreographer.FrameCallback回调,回调方法doFrame在每次Vsync信号即未来临时被调用,上层监听此回调接口并计算两次回调以前的时间差,Android系统默认的刷新频率是16.6ms一次,时间差除以刷新频率即为掉帧状况。
FPSTracer不一样的点在于其内部能统计一段时间的平均帧率,并定义了帧率好坏的梯度。
# -> FPSTracer.DropStatus
private enum DropStatus {
DROPPED_FROZEN(4), DROPPED_HIGH(3), DROPPED_MIDDLE(2), DROPPED_NORMAL(1), DROPPED_BEST(0);
int index;
DropStatus(int index) {
this.index = index;
}
}
复制代码
核心方法代码片断
# FPSTracer -> doReport
private void doReport() {
LinkedList<Integer> reportList;
synchronized (this.getClass()) {
if (mFrameDataList.isEmpty()) {
return;
}
reportList = mFrameDataList;
mFrameDataList = new LinkedList<>();
}
//数据转储到mPendingReportSet集合中
for (int trueId : reportList) {
int scene = trueId >> 22;
int durTime = trueId & 0x3FFFFF;
LinkedList<Integer> list = mPendingReportSet.get(scene);
if (null == list) {
list = new LinkedList<>();
mPendingReportSet.put(scene, list);
}
list.add(durTime);
}
reportList.clear();
//统计分析
for (int i = 0; i < mPendingReportSet.size(); i++) {
int key = mPendingReportSet.keyAt(i);
LinkedList<Integer> list = mPendingReportSet.get(key);
if (null == list) {
continue;
}
int sumTime = 0;
int markIndex = 0;
int count = 0;
int[] dropLevel = new int[DropStatus.values().length]; // record the level of frames dropped each time
int[] dropSum = new int[DropStatus.values().length]; // record the sum of frames dropped each time
int refreshRate = (int) Constants.DEFAULT_DEVICE_REFRESH_RATE * OFFSET_TO_MS;
for (Integer period : list) {
sumTime += period;
count++;
int tmp = period / refreshRate - 1;
//将掉帧状况写入数组
if (tmp >= Constants.DEFAULT_DROPPED_FROZEN) {
dropLevel[DropStatus.DROPPED_FROZEN.index]++;
dropSum[DropStatus.DROPPED_FROZEN.index] += tmp;
} else if (tmp >= Constants.DEFAULT_DROPPED_HIGH) {
dropLevel[DropStatus.DROPPED_HIGH.index]++;
dropSum[DropStatus.DROPPED_HIGH.index] += tmp;
} else if (tmp >= Constants.DEFAULT_DROPPED_MIDDLE) {
dropLevel[DropStatus.DROPPED_MIDDLE.index]++;
dropSum[DropStatus.DROPPED_MIDDLE.index] += tmp;
} else if (tmp >= Constants.DEFAULT_DROPPED_NORMAL) {
dropLevel[DropStatus.DROPPED_NORMAL.index]++;
dropSum[DropStatus.DROPPED_NORMAL.index] += tmp;
} else {
dropLevel[DropStatus.DROPPED_BEST.index]++;
dropSum[DropStatus.DROPPED_BEST.index] += (tmp < 0 ? 0 : tmp);
}
//达到分片时间 sendReport一次
if (sumTime >= mTraceConfig.getTimeSliceMs() * OFFSET_TO_MS) { // if it reaches report time
float fps = Math.min(60.f, 1000.f * OFFSET_TO_MS * (count - markIndex) / sumTime);
MatrixLog.i(TAG, "scene:%s fps:%s sumTime:%s [%s:%s]", mSceneIdToSceneMap.get(key), fps, sumTime, count, markIndex);
try {
JSONObject dropLevelObject = new JSONObject();
...
JSONObject dropSumObject = new JSONObject();
...
JSONObject resultObject = new JSONObject();
resultObject = DeviceUtil.getDeviceInfo(resultObject, getPlugin().getApplication());
resultObject.put(SharePluginInfo.ISSUE_SCENE, mSceneIdToSceneMap.get(key));
resultObject.put(SharePluginInfo.ISSUE_DROP_LEVEL, dropLevelObject);
resultObject.put(SharePluginInfo.ISSUE_DROP_SUM, dropSumObject);
resultObject.put(SharePluginInfo.ISSUE_FPS, fps);
sendReport(resultObject);
} catch (JSONException e) {
MatrixLog.e(TAG, "json error", e);
}
dropLevel = new int[DropStatus.values().length];
dropSum = new int[DropStatus.values().length];
markIndex = count;
sumTime = 0;
}
}
// delete has reported data
if (markIndex > 0) {
for (int index = 0; index < markIndex; index++) {
list.removeFirst();
}
}
...
}
}
复制代码
整个流程以下
这里有一个细节问题须要处理,好比页面没有静止没有UI绘制任务,这段时间的帧率统计也没意义。事实上,FPSTracer对上述用于存储每帧耗时信息的mFrameDataList的插入作个一个过滤。
# FPSTracer -> doFrame
@Override
public void doFrame(long lastFrameNanos, long frameNanos) {
//知足判断条件才handleDoFrame
if (!isInvalid && isDrawing && isEnterAnimationComplete() && mTraceConfig.isTargetScene(getScene())) {
handleDoFrame(lastFrameNanos, frameNanos, getScene());
}
isDrawing = false;
}
private void handleDoFrame(long lastFrameNanos, long frameNanos, String scene) {
int sceneId;
... //获取scene信息
int trueId = 0x0;
//位运算,将sceneId和耗时信息写入一个int
trueId |= sceneId;
trueId = trueId << 22;
long offset = frameNanos - lastFrameNanos;
trueId |= ((offset / FACTOR) & 0x3FFFFF);
if (offset >= 5 * 1000000000L) {
MatrixLog.w(TAG, "[handleDoFrame] WARNING drop frame! offset:%s scene%s", offset, scene);
}
//添加到mFrameDataList
synchronized (this.getClass()) {
mFrameDataList.add(trueId);
}
}
复制代码
看条件!isInvalid && isDrawing && isEnterAnimationComplete() && mTraceConfig.isTargetScene(getScene())
getDecorView().getViewTreeObserver().addOnDrawListener()
)监听view的绘制,当回调onDraw时将此变量设为true,onFrame结束设置为false。所以处于静止状态的时间段不会统计帧信息。这样真个fps检测流程也就结束了,咱们来看一下官方sample汇总的report展示。
首先要明确的是统计的是应用的启动,这包括application建立过程而不单纯是activity启动。统计触发一次就会销毁,所以若是想统计activity之间跳转的状况需手动获取StartUpTrace并调用onCreate方法。
具体的统计指标以下:
统计项目 | 含义 |
---|---|
appCreateTime | application建立时长 |
betweenCost | application建立完成到第一个Activity create完成 |
activityCreate | activity 执行完super.oncreate()至window获取焦点 |
splashCost | splash界面建立时长 |
allCost | 到主界面window focused总时长 |
isWarnStartUp | 是否为热启动(application存在) |
时间轴大体是这样的:
为了实现上述统计指标须要hook ActivityThread中消息处理内部类H(成员变量mH),它是一个Handler对象,activity的建立与生命周期的处理都是经过它完成的,若是你熟悉activity的启动流程那么对mH成员变量必定不陌生。ApplicationThread做为binder通讯的信使,接收AMS的调度事件,好比scheduleLaunchActivity,此方法内部会经过mH对象发送 H.LAUNCH_ACTIVITY消息,mH接收到此消息便会调用handleLaunchActivity建立activity对象。
这属于Activity启动流程范畴,本篇再也不讨论。重点关注hook动做。
# -> StartUpHacker
public class StartUpHacker {
private static final String TAG = "Matrix.Hacker";
public static boolean isEnterAnimationComplete = false;
public static long sApplicationCreateBeginTime = 0L;
public static int sApplicationCreateBeginMethodIndex = 0;
public static long sApplicationCreateEndTime = 0L;
public static int sApplicationCreateEndMethodIndex = 0;
public static int sApplicationCreateScene = -100;
//此方法被静态代码块调用 在被类resolve时执行
public static void hackSysHandlerCallback() {
try {
sApplicationCreateBeginTime = System.currentTimeMillis();
sApplicationCreateBeginMethodIndex = MethodBeat.getCurIndex();
Class<?> forName = Class.forName("android.app.ActivityThread");
Field field = forName.getDeclaredField("sCurrentActivityThread");
field.setAccessible(true);
Object activityThreadValue = field.get(forName);
Field mH = forName.getDeclaredField("mH");
mH.setAccessible(true);
Object handler = mH.get(activityThreadValue);
Class<?> handlerClass = handler.getClass().getSuperclass();
Field callbackField = handlerClass.getDeclaredField("mCallback");
callbackField.setAccessible(true);
Handler.Callback originalCallback = (Handler.Callback) callbackField.get(handler);
HackCallback callback = new HackCallback(originalCallback);
callbackField.set(handler, callback);
MatrixLog.i(TAG, "hook system handler completed. start:%s", sApplicationCreateBeginTime);
} catch (Exception e) {
MatrixLog.e(TAG, "hook system handler err! %s", e.getCause().toString());
}
}
}
复制代码
代码比较简单,就是取出mH对象内部原有的Handler.Callback,将它换成成新的HackCallback。
# StartUpHacker.HackCallback
private final static class HackCallback implements Handler.Callback {
private final Handler.Callback mOriginalCallback;
HackCallback(Handler.Callback callback) {
this.mOriginalCallback = callback;
}
@Override
public boolean handleMessage(Message msg) {
...
//优先处理 设置一些值
boolean isLaunchActivity = isLaunchActivity(msg);
if (isLaunchActivity) {
StartUpHacker.isEnterAnimationComplete = false;
} else if (msg.what == ENTER_ANIMATION_COMPLETE) {
//记录activity转场动画结束标志
StartUpHacker.isEnterAnimationComplete = true;
}
if (!isCreated) {
if (isLaunchActivity || msg.what == CREATE_SERVICE || msg.what == RECEIVER) {
//以第一个Activity LAUNCH_ACTIVITY消息为止,记录application建立结束时间
StartUpHacker.sApplicationCreateEndTime = SystemClock.uptimeMillis();
StartUpHacker.sApplicationCreateEndMethodIndex = MethodBeat.getCurIndex();
StartUpHacker.sApplicationCreateScene = msg.what;
isCreated = true;
}
}
if (null == mOriginalCallback) {
return false;
}
//最终让原有的callback处理消息
return mOriginalCallback.handleMessage(msg);
}
}
复制代码
了解了hook原理,咱们来看一下统计时间的几个关键节点是如何得到的。
写到这,整个Trace Canary的内容就算大体讲完了,其中涉及的知识点很是多,包括UI绘制流程、Activity启动流程、应用启动流程、打包流程、ASM插桩等等。笔者只是按源码流程大体理出了最核心的内容,分支的技术点大多一笔略过,须要读者自行补充,但愿你们一块儿加油,补足分支的技术栈。