探秘 React Native:线程探索及桥优化

前言

提起React Native,大家会自动联想起 JS 是运行在单线程上,进而在性能优化时忽略在线程上的做功,本篇文章与大家一起了解下 RN 执行涉及到的线程以及发现能提升性能的优化点。

1. React Native 涉及线程

1.1. 线程消息队列的创建流程

React Native引擎CatalystInstanceImpl初始化过程中会创建JS线程NativeModules线程、创建消息队列代理类MessageQueueThreadImpl的对象,并会将它们的引用传递到JS引擎中(C++构成的so文件),方便JS层代码逻辑执行和桥调用。同时 UI 线程的消息队列代理对象也会被创建,以便RN原生侧调用。

  • 创建ReactQueueConfigurationImpl
java 复制代码
public class ReactQueueConfigurationImpl implements ReactQueueConfiguration {

    private final MessageQueueThreadImpl mUIQueueThread;
    private final MessageQueueThreadImpl mNativeModulesQueueThread;
    private final MessageQueueThreadImpl mJSQueueThread;

    // 创建ReactQueueConfigurationImpl对象的静态方法
    // 简化后的代码
    public static ReactQueueConfigurationImpl create(
            ReactQueueConfigurationSpec spec, QueueThreadExceptionHandler exceptionHandler) {

        MessageQueueThreadSpec uiThreadSpec = MessageQueueThreadSpec.mainThreadSpec();
        // 通过标识获取ui线程。以下均会调用MessageQueueThreadImpl#create
        MessageQueueThreadImpl uiThread = MessageQueueThreadImpl.create(uiThreadSpec, exceptionHandler);
        // 创建js线程
        MessageQueueThreadImpl jsThread = MessageQueueThreadImpl.create(spec.getJSQueueThreadSpec(), exceptionHandler);
        // 创建nativeModules线程
        MessageQueueThreadImpl nativeModulesThread = MessageQueueThreadImpl.create(spec.getNativeModulesQueueThreadSpec(), exceptionHandler);
        return new ReactQueueConfigurationImpl(uiThread, nativeModulesThread, jsThread);
    }

    // ...
}
  • 除 UI 线程外的其它线程需要单独创建并初始化 Looper
java 复制代码
public static MessageQueueThreadImpl create(
        MessageQueueThreadSpec spec, QueueThreadExceptionHandler exceptionHandler) {
    switch (spec.getThreadType()) {
        case MAIN_UI:
            // ui线程走这里
            return createForMainThread(spec.getName(), exceptionHandler);
        case NEW_BACKGROUND:
            // js线程和nativeModules线程走这里
            return startNewBackgroundThread(spec.getName(), spec.getStackSize(), exceptionHandler);
        default:
            throw new RuntimeException("Unknown thread type: " + spec.getThreadType());
    }
}
  • 线程创建并初始化 Looper,线程名增加mqt_前缀
java 复制代码
// ui线程------创建消息队列封装
private static MessageQueueThreadImpl createForMainThread(
        String name, QueueThreadExceptionHandler exceptionHandler) {
    Looper mainLooper = Looper.getMainLooper();
    final MessageQueueThreadImpl mqt =
            new MessageQueueThreadImpl(name, mainLooper, exceptionHandler);
    // 次要代码简化...
    return mqt;
}

// 其它线程------创建消息队列封装
private static MessageQueueThreadImpl startNewBackgroundThread(
        final String name, long stackSize, QueueThreadExceptionHandler exceptionHandler) {

    Thread bgThread =
            new Thread(
                    null,
                    new Runnable() {
                        @Override
                        public void run() {
                            Looper.prepare();
                            // 次要代码简化...
                            Looper.loop();
                        }
                    },
                    "mqt_" + name,
                    stackSize);

    bgThread.start();
    // 这里会阻塞,等待线程创建和Looper初始化完成,first是looper
    Pair<Looper, MessageQueueThreadPerfStats> pair = dataFuture.getOrThrow();
    return new MessageQueueThreadImpl(name, pair.first, exceptionHandler, pair.second);
}
  • MessageQueueThreadImpl 类的结构
java 复制代码
@DoNotStrip
public class MessageQueueThreadImpl implements MessageQueueThread {

    private MessageQueueThreadImpl(
            String name,
            Looper looper,
            QueueThreadExceptionHandler exceptionHandler,
            MessageQueueThreadPerfStats stats) {
        mName = name;
        mLooper = looper;
        mHandler = new MessageQueueThreadHandler(looper, exceptionHandler);
        mPerfStats = stats;
        mAssertionErrorMessage = "Expected to be called from the '" + getName() + "' thread!";
    }

    // 本质是通过handler放到线程的消息队列中
    @Override
    public boolean runOnQueue(Runnable runnable) {
        // 无关代码简化...
        mHandler.post(runnable);
        return true;
    }

    @DoNotStrip
    @Override
    public <T> Future<T> callOnQueue(final Callable<T> callable) {
        final SimpleSettableFuture<T> future = new SimpleSettableFuture<>();
        runOnQueue(
                new Runnable() {
                    @Override
                    public void run() {
                        try {
                            future.set(callable.call());
                        } catch (Exception e) {
                            future.setException(e);
                        }
                    }
                });
        return future;
    }

    // ....
}

1.2. mqt_js线程

所有的 JS 代码执行都是使用的这个线程。 除此之外同步桥调用也会使用该线程,可以通过 ReactMethod 注解的 isBlockingSynchronousMethod 参数设置。

java 复制代码
@ReactModule(name = "MyNativeModule")
class MyNativeModule : ReactContextBaseJavaModule() {
    override fun getName(): String {
        return "MyNativeModule"
    }

    // 设为同步桥
    @ReactMethod(isBlockingSynchronousMethod = true)
    fun sayHello(name: String) {
        Thread.sleep(3000)
        Log.i("MyNativeModule", "native侧 sayHello方法执行线程:" + Thread.currentThread().name)
    }
}

初次使用的时候可能会怀疑这里真的和 JS 侧的线程是同一个吗?咱们测试一下(sayHello原生桥中加了3000ms的休眠)。

在JS侧方法中调用同步桥并打印下执行耗时:

javascript 复制代码
const startTime = Date.now();

NativeModules.MyNativeModule.sayHello('1111');

console.log('JS侧 sayHello桥方法执行耗时' + (Date.now() - startTime));

打印日志:

native侧 sayHello方法执行线程:mqt_js

JS侧 sayHello桥方法执行耗时3002

从日志可以看出执行JS代码与Native同步桥方法的线程是同一个,并且同步桥会完全阻塞JS逻辑的执行

1.3. mqt_native_modules线程

该线程主要用于执行Native异步桥方法。

测试一下:

JavaModuleWrapperinvoke方法中打印线程名称:

java 复制代码
public class JavaModuleWrapper {

    // ...

    @DoNotStrip
    public void invoke(int methodId, ReadableNativeArray parameters) {
        // ...
        android.util.Log.i("RN测试", "线程:" + Thread.currentThread().getName() + " 调用方法:" + ((JavaMethodWrapper)mMethods.get(methodId)).getMethod().getName() + "  参数:" + parameters);
        mMethods.get(methodId).invoke(mJSInstance, parameters);
    }
}

结果:

从日志中发现自定义桥和视图相关的桥都是交由 mqt_native_modules 线程处理的。这里的线程处理是不区分优先级,其它桥的执行是会影响视图桥的执行(会影响首屏耗时) ,做性能优化时应考虑将非必需桥放到首屏之后调用。

1.4. 同步桥与异步桥

桥类型 线程切换过程 适用场景 备注
同步桥 一直JS线程 桥方法运行耗时短 且 短时间内桥调用量级极小 JS线程自己处理,减少线程切换成本;线下测试同步桥是异步桥执行耗时的30%左右。
异步桥 JS线程(JS侧) → NativeModules线程(Native侧) → JS线程(JS侧) 桥方法运行耗时长 或 短时间内桥调用量级较大 JS线程与NativeModules线程并发处理事件

2. 检测耗时桥

Android支持通过Trace查看桥执行的耗时(只能查看异步桥),针对执行耗时较高的桥,建议另建线程调度,避免影响待执行队列中其它桥的执行(尤其不能影响UI相关的桥)。

2.1. 使用Perfetto录制Trace

2.1.1. Perfetto录制配置Perfetto UI

2.1.2. 安装目标apk

2.1.3. 执行命令 adb kill-server

2.1.4. 关闭Android Studio(AS的adb与Perfetto的adb连接冲突)

2.1.5. 设备重连数据线

2.1.6. Perfetto关联上设备 ui.perfetto.dev/#!/record/t...

2.1.7. 点击上图中的录制即可

在录制期间打开app中我们想检测的页面。

2.1.7. 录制完成后会自动打开trace分析页面

2.2. 通过SQL查Trace

Perfetto支持通过SQL查询Trace,可以查桥执行耗时、桥调用顺序等。

举例:按降序查桥执行耗时

在输入框中打英文:,然后输入SQL

select * from slices where name like '%callJavaModuleMethod%' order by dur desc。

dur字段便是桥方法在native执行的耗时,单位是微秒。

如果期望看更细节的桥信息,可以在代码的ApplicationonCreate增加配置。

ini 复制代码
// 丰富桥执行trace的配置
SystraceMessage.INCLUDE_ARGS = true

然后重新录制即可。

name中会展示详细的桥方法名,逐个排查耗时是否符合预期即可。

2.3. 线上统计

通过Trace只能看出大概桥耗时情况,线上case错综复杂,高中低端机可能出现不同的表现。因此可以在线上抽样记录耗时情况,在合适时机批量上报。

最后

希望本篇文章能给大家带来一些优化思路,也欢迎大家在评论区切磋交流。

推荐阅读:

相关推荐
MK-mm7 分钟前
CSS盒子 flex弹性布局
前端·css·html
陈旭金-小金子16 分钟前
发现 Kotlin MultiPlatform 的一点小变化
android·开发语言·kotlin
小小小小宇20 分钟前
CSP的使用
前端
sunbyte20 分钟前
50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | AnimatedNavigation(动态导航)
前端·javascript·vue.js·tailwindcss
ifanatic30 分钟前
[每周一更]-(第147期):使用 Go 语言实现 JSON Web Token (JWT)
前端·golang·json
烛阴31 分钟前
深入浅出地理解Python元类【从入门到精通】
前端·python
米粒宝的爸爸34 分钟前
uniapp中vue3 ,uview-plus使用!
前端·vue.js·uni-app
JustHappy1 小时前
啥是Hooks?为啥要用Hooks?Hooks该怎么用?像是Vue中的什么?React Hooks的使用姿势(下)
前端·javascript·react.js
董先生_ad986ad1 小时前
C# 解析 URL URI 中的参数
前端·c#
江城开朗的豌豆1 小时前
Vue中Token存储那点事儿:从localStorage到内存的避坑指南
前端·javascript·vue.js