Matrix源码分析(六)之 ActivityThreadHacker工作原理

在前面分析 AppMethodBeat 的过程中有介绍过 ActivityThreadHacker ,但是在那篇文章中没有仔细的介绍,但是接下来如果要分析StartupTracer 又用到了 ActivityThreadHacker ,那这篇文章就先来说说 ActivityThreadHacker 的工作原理,

在讲 ActivityThreadHacker 他的工作原理之前,需要先了解一个因为 SharedPreferences 的工作原理引起的一个系统性的bug, 那就是为什么 SP 的apply方法分明就是异步的缺会导致主线程卡住引ANR的问题

先来复现一下事故现场看看 SharedPreferencesImpl 的 apply 的方法

API 21

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

    // 将结果写入到内存,比较重要的,在这里创建了一个 CountDownLatch ,每次写入都会创建
    // 所以这个 CountDownLatch 给的等待个数是1
    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 内的链表中
    QueuedWork.addFinisher(awaitCommit);
    //创建移除的任务
    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };
    // 开始写磁盘
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

    // Okay to notify the listeners before it's hit disk
    // because the listeners should always get the same
    // SharedPreferences instance back, which has the
    // changes reflected in memory.
    notifyListeners(mcr);
}

在apply方法中,先是将创建的事务写入到了内存中,并返回了修改后的结果,利用这个结果中的 CountDownLatch 创建了一个任务,放入到了 QueuedWork 中的链表里面,然后开始写磁盘

API 21

java 复制代码
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    //创建写磁盘的任务                          
    final Runnable writeToDiskRunnable = new Runnable() {
            public void run() {
                // 使用同步锁写文件
                synchronized (mWritingToDiskLock) {
                    writeToFile(mcr);
                }
                //类锁改次数
                synchronized (SharedPreferencesImpl.this) {
                    mDiskWritesInFlight--;
                }
                //通知 上层 CountDownLatch 开始等待
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };

    final boolean isFromSyncCommit = (postWriteRunnable == null);

    // Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (SharedPreferencesImpl.this) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }

    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

这里的思路也比较简单,那就是在使用线程池工作前,先使用写文件的锁来写文件,在使用类锁修改写的次数,最后在通知到上层的 CountDownLatch 让他等待,只有在设置结果后才会释放CountDownLatch 的等待

ini 复制代码
void setDiskWriteResult(boolean wasWritten, boolean result) {
    this.wasWritten = wasWritten;
    writeToDiskResult = result;
    writtenToDiskLatch.countDown();
}

我们再去看看引起 ANR 的地方 ActivityThread

java 复制代码
private void handleStopActivity(IBinder token, boolean show, int configChanges) {
    ActivityClientRecord r = mActivities.get(token);
    r.activity.mConfigChangeFlags |= configChanges;

    StopInfo info = new StopInfo();
    performStopActivityInner(r, info, show, true);

    if (localLOGV) Slog.v(
        TAG, "Finishing stop of " + r + ": show=" + show
        + " win=" + r.window);

    updateVisibility(r, show);

    //关键逻辑
    if (!r.isPreHoneycomb()) {
        //罪魁祸首
        QueuedWork.waitToFinish();
    }

    info.activity = r;
    info.state = r.state;
    info.persistentState = r.persistentState;
    mH.post(info);
    mSomeActivitiesChanged = true;
}

// sdk 的 targetVersion 是否小于11,很明显现在都大于11
public boolean isPreHoneycomb() {
    if (activity != null) {
        return activity.getApplicationInfo().targetSdkVersion
                < android.os.Build.VERSION_CODES.HONEYCOMB;
    }
    return false;
}

我这里截取的是 handleStopActivity 中的逻辑,但并不代表着 只有他在使用,这里放一下在 ActivityThread 中谁在用

那就来看看这个waitToFinish都干了什么东西

csharp 复制代码
public static void waitToFinish() {
    Runnable toFinish;
    while ((toFinish = sPendingWorkFinishers.poll()) != null) {
        toFinish.run();
    }
}

很简单的一段代码,如果 sPendingWorkFinishers 这个 Queue 中有任务先执行这个任务,这就导致了主线程中进行了等待了

那么这个任务是什么任务呢

csharp 复制代码
public static void add(Runnable finisher) {
    sPendingWorkFinishers.add(finisher);
}

没错就是我们在sp apply 的时候添加进去的 CountDownLatch 的等待任务,如果这个任务被行了那么主线程就卡死在这里了,如果我们同时提交的sp任务比较多或者 由于当时环境问题导致 写文件速度比较慢,都会导致 ANR

API29

再来看看API 29的代码

ini 复制代码
public static void waitToFinish() {
    long startTime = System.currentTimeMillis();
    boolean hadMessages = false;

    Handler handler = getHandler();

    synchronized (sLock) {
        if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
            // 这里移除了部分延迟工作的任务
            handler.removeMessages(QueuedWorkHandler.MSG_RUN);

            if (DEBUG) {
                hadMessages = true;
                Log.d(LOG_TAG, "waiting");
            }
        }

        // We should not delay any work as this might delay the finishers
        sCanDelay = false;
    }
    
    // 允许在主线程中写文件,并返回旧的策略
    StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
    try {
        //把任务取出来直接在当前线程处理
        processPendingWork();
    } finally {
    //恢复旧的策略
        StrictMode.setThreadPolicy(oldPolicy);
    }

    try {
        while (true) {
            Runnable finisher;

            synchronized (sLock) {
                finisher = sFinishers.poll();
            }

            if (finisher == null) {
                break;
            }

            finisher.run();
        }
    } finally {
        sCanDelay = true;
    }

    synchronized (sLock) {
        long waitTime = System.currentTimeMillis() - startTime;

        if (waitTime > 0 || hadMessages) {
            mWaitTimes.add(Long.valueOf(waitTime).intValue());
            mNumWaits++;

            if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
                mWaitTimes.log(LOG_TAG, "waited: ");
            }
        }
    }
}

很明显这里的代码量一下子增加了非常多,但是基础的逻辑没有太大的变化,显示移除了由于 handler 中的 QueuedWorkHandler.MSG_RUN 消息,修改严格模式的策略将任务在当前线程中执行,在修改后再恢复到之前的模式,最后还是遍历这个队列 ,按次执行

接下来我们的主角就入场了 ActivityThreadHacker

在 ActivityThread 所有的生命周期事件都是通过 Handler 来处理的,并且 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);
    }
}

那就是如果在 mCallback 不为空的情况下,所有的消息是需要先经过 mCallback.handleMessage(msg) 来预处理一下的, 而 ActivityThreadHacker 就是通过 Hook 替换原始Handler ,判断对应的生命周期事件,清空 sFinishers 中的任务,来达到优化ANR 的目的

下面来看一下代码

ini 复制代码
public static void hackSysHandlerCallback() {
    try {
        sApplicationCreateBeginTime = SystemClock.uptimeMillis();
        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);
            Callback originalCallback = (Callback)callbackField.get(handler);
            ActivityThreadHacker.HackCallback callback = new ActivityThreadHacker.HackCallback(originalCallback);
            callbackField.set(handler, callback);
        }

        MatrixLog.i("Matrix.ActivityThreadHacker", "hook system handler completed. start:%s SDK_INT:%s", new Object[]{sApplicationCreateBeginTime, VERSION.SDK_INT});
    } catch (Exception var9) {
        MatrixLog.e("Matrix.ActivityThreadHacker", "hook system handler err! %s", new Object[]{var9.getCause().toString()});
    }

}

使用自己的 ActivityThreadHacker.HackCallback 包装一下 ActivityThread 中的 mH 中的 Callback来打包目的,在来看看他是如何清空这个队列的

typescript 复制代码
private void fix() {
    try {
        Class cls = Class.forName("android.app.QueuedWork");
        Field field = cls.getDeclaredField("sPendingWorkFinishers");
        if (field != null) {
            field.setAccessible(true);
            ConcurrentLinkedQueue<Runnable> runnables = (ConcurrentLinkedQueue)field.get((Object)null);
            runnables.clear();
            MatrixLog.i("Matrix.ActivityThreadHacker", "Fix SP ANR sPendingWorkFinishers.clear successful", new Object[0]);
        }
    } catch (ClassNotFoundException var4) {
        MatrixLog.e("Matrix.ActivityThreadHacker", "Fix SP ANR ClassNotFoundException = " + var4.getMessage(), new Object[0]);
        var4.printStackTrace();
    } catch (IllegalAccessException var5) {
        MatrixLog.e("Matrix.ActivityThreadHacker", "Fix SP ANR IllegalAccessException =" + var5.getMessage(), new Object[0]);
        var5.printStackTrace();
    } catch (NoSuchFieldException var6) {
        MatrixLog.e("Matrix.ActivityThreadHacker", "Fix SP ANR NoSuchFieldException = " + var6.getMessage(), new Object[0]);
        var6.printStackTrace();
    } catch (Exception var7) {
        MatrixLog.e("Matrix.ActivityThreadHacker", "Fix SP ANR Exception = " + var7.getMessage(), new Object[0]);
        var7.printStackTrace();
    }

}

反射 QueuedWork 中的 sPendingWorkFinishers 并将他清空,

但是到了 26以后由于属性名字就变了,而且需要在特定的生命周期事件才能清空,其他的清空了也没有太大的意义

ini 复制代码
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();
        }
}

在查看这个问题的处理方式时还找了今日头条的解决方案,他们的解决方案有点类似 Matrix 中的 IdleHandlerLagTracer 的方式,他们是替换了这个 QueueWrok 中的 queue ,让他的 poll 方法返回null

对于 sp 的apply 问题导致的anr 的产生,真的是由于 sp 的设计缺陷导致的吗,个人感觉应该是过度使用sp而导致的,而且在使用sp的过程中为了减少文件大小,sp的key一定要精简,并且针对sp 的分包也是非常关键的

其实这篇文章真正想要引出来的主角就是 以下代码

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();
            }
        }
    }
}

这段代码的就引出了我们下一篇文章的故事 StartUpTracker ,Callback 中接收到 actvity service receiver等的生命皱起了,证明 ActivityThread 对于Application的初始化是必然已经完成了 ,并且这个方法只会执行一次,

相关推荐
大白要努力!21 分钟前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee27 分钟前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood39 分钟前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-4 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
Eastsea.Chen6 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年13 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
建群新人小猿16 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神17 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛17 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法18 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526