Android性能优化系列-腾讯matrix-ActivityThreadHacker实现原理

前言

上文Android性能优化系列-腾讯matrix-TracePlugin启动速度优化之StartupTracer源码分析提到,ActivityThreadHacker.addListener(this),通过将当前plugin加入到监听中,可以拿到application创建完成的回调-onApplicationCreateEnd()。那么它是怎么做到的,今天我们能简单分析下ActivityThreadHacker的实现原理。

IApplicationCreateListener

首先我们打开ActivityThreadHacker源码,可以看到在类中有一个集合,用来存储IApplicationCreateListener对象。

swift 复制代码
private static final HashSet<IApplicationCreateListener> listeners = new HashSet<>();

还有两个方法负责添加和移除IApplicationCreateListener对象。

csharp 复制代码
public static void addListener(IApplicationCreateListener listener) {
    synchronized (listeners) {
        listeners.add(listener);
    }
}

public static void removeListener(IApplicationCreateListener listener) {
    synchronized (listeners) {
        listeners.remove(listener);
    }
}

而IApplicationCreateListener的回调方法正是onApplicationCreateEnd。

hackSysHandlerCallback

找到hackSysHandlerCallback方法,搜索它被调用的地方

csharp 复制代码
public static void hackSysHandlerCallback()

会找到AppMethodBeat类中的realExecute方法,而realExecute又被AppMethodBeat自身的i()方法调用,AppMethodBeat的i()方法是在编译期间被插桩到每一个符合插桩条件的方法的入口位置的,详见Android性能优化系列-腾讯matrix-卡顿监控-gradle插件- 字节码插桩代码分析

status表示AppMethodBeat自身运行的状态,它一共有6种状态,而它状态的切换是跟随TracePlugin进行的,TracePlugin start的时候,AppMethodBeat进入started状态,TracePlugin stop的时候,AppMethodBeat进入stop状态。默认状态下是STATUS_DEFAULT,也就是等于Integer.MAX_VALUE。

scss 复制代码
public static void i(int methodId) {
    //status表示AppMethodBeat自身运行的状态,他一共有6种状态,详见下方
    if (status <= STATUS_STOPPED) {
        return;
    }
    if (methodId >= METHOD_ID_MAX) {
        return;
    }
    //所以这里当第一个被插桩的方法调用时,由于status是满足STATUS_DEFAULT状态的,所以下面的方法被执行,
    //执行之后status状态被赋值为STATUS_READY,后边便不会再执行,于是保证了realExecute只会执行一次
    if (status == STATUS_DEFAULT) {
        synchronized (statusLock) {
            if (status == STATUS_DEFAULT) {
                realExecute();
                status = STATUS_READY;
            }
        }
    }
    ...
}

status表示AppMethodBeat自身运行的状态,他一共有6种状态:

arduino 复制代码
private static final int STATUS_DEFAULT = Integer.MAX_VALUE;
private static final int STATUS_STARTED = 2;
private static final int STATUS_READY = 1;
private static final int STATUS_STOPPED = -1;
private static final int STATUS_EXPIRED_START = -2;
private static final int STATUS_OUT_RELEASE = -3;

realExecute

看下realExecute, 我们只看和ActivityThreadHacker相关的,其他的内容会在AppMethodBeat类分析时再深入来看。这里调用了hackSysHandlerCallback方法,从上边我们知道realExecute只会执行一次,自然hackSysHandlerCallback也只会执行一次。

csharp 复制代码
private static void realExecute() {
    ...
    ActivityThreadHacker.hackSysHandlerCallback();
    ...
}

hackSysHandlerCallback

方法主要是在进行hook, 通过反射拿到当前ActivityThread对象中的mH,mH是Handler对象,然后又从Handler中反射拿到mCallback,对mCallback进行代理,并原有的Callback修改为代理后的Callback-HackCallback。

ini 复制代码
public static void hackSysHandlerCallback() {
    try {
        //将第一个方法执行的时间作为sApplicationCreateBeginTime创建的时间来记录
        sApplicationCreateBeginTime = SystemClock.uptimeMillis();
        //从AppMethodBeat拿一个标记出来,用于后边从AppMethodBeat取数据,具体内容后期的文章会专门分析AppMethodBeat
        sApplicationCreateBeginMethodIndex = AppMethodBeat.getInstance().maskIndex("ApplicationCreateBeginMethodIndex");
        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();
        if (null != handlerClass) {
            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 SDK_INT:%s", sApplicationCreateBeginTime, Build.VERSION.SDK_INT);
    } catch (Exception e) {
        MatrixLog.e(TAG, "hook system handler err! %s", e.getCause().toString());
    }
}

我们知道Handler中在每个消息被处理的时候,有这么一个操作。

less 复制代码
public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

HackCallback

当matrix将代理callback注入到Handler后,那么所有的handleMessage都会被HackCallback拦截到,于是我们进入HackCallback一探究竟。HackCallback继承自Handler.Callback,我们直接看它的handleMessage方法。

handleMessage

scss 复制代码
public boolean handleMessage(Message msg) {
    //第一步
    if (IssueFixConfig.getsInstance().isEnableFixSpApply()) {
        if (Build.VERSION.SDK_INT >= 21 && Build.VERSION.SDK_INT <= 25) {
            if (msg.what == SERIVCE_ARGS || msg.what == STOP_SERVICE
                    || msg.what == STOP_ACTIVITY_SHOW || msg.what == STOP_ACTIVITY_HIDE
                    || msg.what == SLEEPING) {
                    MatrixLog.i(TAG, "Fix SP ANR is enabled");
                    fix();
                }
        }
    }

    if (!AppMethodBeat.isRealTrace()) {
        return null != mOriginalCallback && mOriginalCallback.handleMessage(msg);
    }
    //第二步
    boolean isLaunchActivity = isLaunchActivity(msg);

    if (hasPrint > 0) {
        MatrixLog.i(TAG, "[handleMessage] msg.what:%s begin:%s isLaunchActivity:%s SDK_INT=%s", msg.what, SystemClock.uptimeMillis(), isLaunchActivity, Build.VERSION.SDK_INT);
        hasPrint--;
    }

    if (!isCreated) {
        if (isLaunchActivity || msg.what == CREATE_SERVICE
                || msg.what == RECEIVER) { // todo for provider
            ActivityThreadHacker.sApplicationCreateEndTime = SystemClock.uptimeMillis();
            ActivityThreadHacker.sApplicationCreateScene = msg.what;
            isCreated = true;
            sIsCreatedByLaunchActivity = isLaunchActivity;
            MatrixLog.i(TAG, "application create end, sApplicationCreateScene:%d, isLaunchActivity:%s", msg.what, isLaunchActivity);
            synchronized (listeners) {
                for (IApplicationCreateListener listener : listeners) {
                    listener.onApplicationCreateEnd();
                }
            }
        }
    }
    return null != mOriginalCallback && mOriginalCallback.handleMessage(msg);
}

第一步fix

handleMessage中我们分两步来看,第一步,针对sharePreference的一个处理,当系统版本在5.0-8.0之间时(包含5.0不包含8.0),当消息的what为指定的类型时,会去执行一个fix的操作,而fix方法体如下,看起来是拿到QueuedWork类中的sPendingWorkFinishers,然后将其存储的内容清空。这是什么逻辑?

ini 复制代码
private void fix() {
    Class cls = Class.forName("android.app.QueuedWork");
    Field field = cls.getDeclaredField("sPendingWorkFinishers");
    if (field != null) {
        field.setAccessible(true);
        ConcurrentLinkedQueue<Runnable> runnables = (ConcurrentLinkedQueue<Runnable>) field.get(null);
        runnables.clear();
    }
}

其实这个涉及到一个SharePreference潜在的风险点。SharePreference本身是一个轻量型的存储工具,但是在很多应用中被滥用,导致耗时的发生。

Google 系统为了确保数据的跨进程完整性,前期应用可以使用 sp 来做跨进程通信,在组件销毁或其他生命周期的同时为了确保当前这个写入任务必须在当前这个组件的生命周期完成写入,此时主线程会在组件销毁或者组件暂停的生命周期内等待 sp 完全写入到对应的文件中,而这个等待写入的操作就是在QueuedWork.waitToFinish()中,如activity暂停的时候源码,onPause执行后,就会进入QueuedWork.waitToFinish(),等待sp写入完成。

typescript 复制代码
@Override
public void handlePauseActivity(ActivityClientRecord r, boolean finished, boolean userLeaving,
        int configChanges, PendingTransactionActions pendingActions, String reason) {
    performPauseActivity(r, finished, reason, pendingActions);
    // Make sure any pending writes are now committed.
    if (r.isPreHoneycomb()) {
        QueuedWork.waitToFinish();
    }
}

所有会执行相关操作的消息类型如下,也就是上边的if判断条件,也就是说这种消息执行时,都可能导致长时间的等待sp写入操作:

arduino 复制代码
public static final int SERVICE_ARGS = 115;
public static final int STOP_SERVICE = 116;
public static final int PAUSE_ACTIVITY = 101;
public static final int STOP_ACTIVITY_SHOW = 103;
public static final int SLEEPING  = 137;

简单看下QueuedWork.waitToFinish(),它内部是从集合中拿到所有Runnable,遍历执行run方法,那么这些Runnable是什么时候放进去的?

我们知道SharedPreferences提供了两种写入文件的方式,commit同步写入,apply异步写入,其实就发生在apply调用后,我们看下apply:

java 复制代码
public void apply() {
    final long startTime = System.currentTimeMillis();

    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                    //执行的时候等待完成
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }

                if (DEBUG && mcr.wasWritten) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " applied after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
        };

    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };
    //加入集合
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
}
arduino 复制代码
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    ...
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

apply异步的执行,是将runnable放入了QueuedWork中的队列去执行,当未执行完时,加入这时Activity进入了onPause状态,那么就是一直等待这些任务执行完成,从而可能出现卡住的情况。

所以这个时候我们就清楚了这个fix是在做什么了,它主动将队列中的任务清空,防止卡在这里。

第二步回调onApplicationCreateEnd

第二步的逻辑就很直接了,当第一次执行,并且满足以下条件之一时,会回调onApplicationCreateEnd方法,作为application创建完成的时机,记录时间戳。

  1. 当前是在启动activity
  2. 当前是在创建service
  3. 当前是在启动receiver
  4. 当然provider也是可以的,只不过如注释所说,并没有添加

由isCreated这个变量的控制可知,此逻辑只会执行一次,所以onApplicationCreateEnd自然也只会回调一次。

ini 复制代码
if (!isCreated) {
    if (isLaunchActivity || msg.what == CREATE_SERVICE
            || msg.what == RECEIVER) { // todo for provider
        ActivityThreadHacker.sApplicationCreateEndTime = SystemClock.uptimeMillis();
        ActivityThreadHacker.sApplicationCreateScene = msg.what;
        isCreated = true;
        sIsCreatedByLaunchActivity = isLaunchActivity;
        MatrixLog.i(TAG, "application create end, sApplicationCreateScene:%d, isLaunchActivity:%s", msg.what, isLaunchActivity);
        synchronized (listeners) {
            for (IApplicationCreateListener listener : listeners) {
                listener.onApplicationCreateEnd();
            }
        }
    }
}

总结

回到我们最初的问题ActivityThreadHacker.addListener(this),通过将当前plugin加入到监听中,可以拿到application创建完成的回调-onApplicationCreateEnd(),它是怎么做到的?

很明显,ActivityThreadHacker借助了AppMethodBeat类i方法的首次调用,编译期AppMethodBeat i方法被插桩到每一个方法的入口处,从而当AppMethodBeat 的i方法首次调用的时候, 将HackCallback注入到ActivityThread的Handler中的mCallback上,从而实现拦截所有Message,通过判断收到的Message类型,当message满足以下条件之一时,将此时机作为application创建完成的时机,回调onApplicationCreateEnd方法。

  1. 当前是在启动activity
  2. 当前是在创建service
  3. 当前是在启动receiver
  4. 当然provider也是可以的,只不过如注释所说,并没有添加

同时它也顺便处理了SharedPreferences导致的卡顿问题。

相关推荐
拭心4 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
fantasy_arch5 小时前
CPU性能优化-磁盘空间和解析时间
网络·性能优化
带电的小王6 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡7 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道7 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
码农老起7 小时前
企业如何通过TDSQL实现高效数据库迁移与性能优化
数据库·性能优化
java_heartLake7 小时前
Vue3之性能优化
javascript·vue.js·性能优化
arnold668 小时前
探索 ElasticSearch:性能优化之道
大数据·elasticsearch·性能优化
阿甘知识库8 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道8 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频