JS React Native / Expo:移动应用开发中的性能优化与原生模块桥接

各位观众老爷们,大家好!我是今天的主讲人,咱们今天聊聊React Native/Expo移动应用开发中的性能优化和原生模块桥接这两大块骨头。 这俩东西,一个是让你的应用跑得更快更流畅,另一个是让你能用上手机上那些原生的、酷炫的功能。准备好了吗?咱们这就开整!

第一部分:性能优化——让你的应用飞起来

React Native 虽然号称“一次编写,到处运行”,但如果优化不到位,很容易出现卡顿、掉帧的情况。毕竟,JavaScript 解释执行的速度和直接跑原生代码还是有差距的。所以,性能优化是 React Native 开发中绕不开的一道坎。

  • 1. 渲染优化:该省省,该花花

    • 1.1 减少不必要的渲染:PureComponent 和 React.memo

      React 默认情况下,父组件更新,子组件也会跟着更新,即使子组件的 props 并没有改变。这会造成不必要的渲染,浪费 CPU 资源。

      • PureComponent: 适用于 class 组件,它会自动进行浅比较,只有当 props 或 state 改变时才重新渲染。

        import React, { PureComponent } from 'react';
        
        class MyComponent extends PureComponent {
          render() {
            console.log('MyComponent rendered!');
            return (
              <div>
                {this.props.name} - {this.props.age}
              </div>
            );
          }
        }
        
        export default MyComponent;
        
        // 使用示例
        <MyComponent name="张三" age={20} />
        <MyComponent name="张三" age={20} /> // 不会重新渲染,因为 props 没有改变
        <MyComponent name="李四" age={22} /> // 会重新渲染,因为 props 改变了
      • React.memo: 适用于函数组件,它接收一个组件作为参数,并返回一个记忆化的组件。它也会进行浅比较,只有当 props 改变时才重新渲染。

        import React from 'react';
        
        const MyComponent = React.memo(function MyComponent(props) {
          console.log('MyComponent rendered!');
          return (
            <div>
              {props.name} - {props.age}
            </div>
          );
        });
        
        export default MyComponent;
        
        // 使用示例
        <MyComponent name="张三" age={20} />
        <MyComponent name="张三" age={20} /> // 不会重新渲染,因为 props 没有改变
        <MyComponent name="李四" age={22} /> // 会重新渲染,因为 props 改变了

        注意点: 浅比较只比较 props 和 state 的引用地址,如果 props 是一个对象或者数组,即使内容改变了,但引用地址没变,PureComponent 和 React.memo 仍然不会重新渲染。如果需要深度比较,可以自己实现 shouldComponentUpdate 或者使用 memo 的第二个参数,一个自定义的比较函数。

    • 1.2 避免在 render 函数中创建新的对象或函数

      在 render 函数中创建新的对象或函数,会导致每次渲染都会创建一个新的对象或函数,即使内容相同,也会被认为是不相等的,从而触发不必要的渲染。

      import React, { useState } from 'react';
      
      function MyComponent() {
        const [count, setCount] = useState(0);
      
        // 错误示范:每次渲染都会创建一个新的对象
        const style = { color: 'red' };
      
        // 正确示范:在组件外部创建对象
        // const style = { color: 'red' };
      
        // 错误示范:每次渲染都会创建一个新的函数
        const handleClick = () => {
          setCount(count + 1);
        };
      
        // 正确示范:使用 useCallback 缓存函数
        // const handleClick = useCallback(() => {
        //   setCount(count + 1);
        // }, [count]);
      
        return (
          <div style={style} onClick={handleClick}>
            Count: {count}
          </div>
        );
      }
      
      export default MyComponent;
      
      // 使用 useCallback 的例子
      import React, { useState, useCallback } from 'react';
      
      function MyComponent() {
        const [count, setCount] = useState(0);
      
        const handleClick = useCallback(() => {
          setCount(count + 1);
        }, [count]); // 依赖项,只有 count 改变时才会重新创建函数
      
        return (
          <div onClick={handleClick}>
            Count: {count}
          </div>
        );
      }
      
      export default MyComponent;
      • useCallback: 用于缓存函数,只有当依赖项改变时才会重新创建函数。
      • useMemo: 用于缓存计算结果,只有当依赖项改变时才会重新计算。
    • 1.3 使用 FlatList 或 SectionList 渲染大量数据

      FlatListSectionList 是 React Native 提供的专门用于渲染列表的组件,它们具有以下优点:

      • 虚拟化: 只渲染屏幕可见的元素,而不是一次性渲染所有元素。
      • 回收机制: 当元素滑出屏幕时,会被回收并用于渲染新的元素。
      • 性能优化: 通过优化渲染过程,提高列表的滚动性能。
      import React from 'react';
      import { FlatList, StyleSheet, Text, View } from 'react-native';
      
      const data = Array.from({ length: 1000 }, (_, i) => ({ id: i, title: `Item ${i}` }));
      
      const Item = ({ title }) => (
        <View style={styles.item}>
          <Text style={styles.title}>{title}</Text>
        </View>
      );
      
      const renderItem = ({ item }) => (
        <Item title={item.title} />
      );
      
      const App = () => {
        return (
          <FlatList
            data={data}
            renderItem={renderItem}
            keyExtractor={item => item.id}
          />
        );
      };
      
      const styles = StyleSheet.create({
        item: {
          backgroundColor: '#f9c2ff',
          padding: 20,
          marginVertical: 8,
          marginHorizontal: 16,
        },
        title: {
          fontSize: 32,
        },
      });
      
      export default App;
      • keyExtractor: 用于为每个列表项提供一个唯一的 key,这有助于 React Native 更好地识别和更新列表项。
      • getItemLayout: 用于预先计算每个列表项的高度,这可以提高列表的滚动性能。
  • 2. JavaScript 优化:能少跑就少跑

    • 2.1 避免在循环中创建新的对象或函数

      和 render 函数类似,在循环中创建新的对象或函数也会导致不必要的内存分配和垃圾回收,影响性能。

      // 错误示范
      const myArray = [];
      for (let i = 0; i < 1000; i++) {
        myArray.push({ id: i, value: Math.random() }); // 每次循环都创建一个新的对象
      }
      
      // 正确示范
      const myArray = Array.from({ length: 1000 }, (_, i) => ({ id: i, value: Math.random() }));
    • 2.2 使用 Lodash 或 Ramda 等工具库

      这些工具库提供了许多优化过的函数,可以避免自己写一些低效的代码。例如,可以使用 _.memoize 缓存函数的结果,可以使用 _.debounce_.throttle 控制函数的执行频率。

      import { memoize } from 'lodash';
      
      const expensiveFunction = (x) => {
        console.log('Calculating...');
        return x * x;
      };
      
      const memoizedFunction = memoize(expensiveFunction);
      
      console.log(memoizedFunction(5)); // Calculating... 25
      console.log(memoizedFunction(5)); // 25 (从缓存中获取)
    • 2.3 使用 Immutable.js 等不可变数据结构

      不可变数据结构可以避免直接修改数据,从而避免触发不必要的渲染。

      import { Map } from 'immutable';
      
      const myMap = Map({ name: '张三', age: 20 });
      
      const updatedMap = myMap.set('age', 22); // 创建一个新的 Map,而不是修改原来的 Map
      
      console.log(myMap.get('age')); // 20
      console.log(updatedMap.get('age')); // 22
  • 3. 网络优化:能快则快,能省则省

    • 3.1 使用 CDN 加速静态资源

      将图片、字体等静态资源放到 CDN 上,可以提高加载速度。

    • 3.2 压缩图片

      使用工具压缩图片,可以减少图片的大小,从而减少网络传输时间。

    • 3.3 使用 Gzip 压缩

      对网络传输的数据进行 Gzip 压缩,可以减少数据的大小,从而减少网络传输时间。

    • 3.4 缓存数据

      对网络请求的结果进行缓存,可以避免重复请求,提高性能。可以使用 AsyncStorageRedux Persist 等工具进行缓存。

      import AsyncStorage from '@react-native-async-storage/async-storage';
      
      const storeData = async (key, value) => {
        try {
          const jsonValue = JSON.stringify(value)
          await AsyncStorage.setItem(key, jsonValue)
        } catch (e) {
          // saving error
        }
      }
      
      const getData = async (key) => {
        try {
          const jsonValue = await AsyncStorage.getItem(key)
          return jsonValue != null ? JSON.parse(jsonValue) : null;
        } catch(e) {
          // error reading value
        }
      }
  • 4. 其他优化技巧

    • 4.1 使用 Chrome DevTools 进行性能分析

      Chrome DevTools 提供了强大的性能分析工具,可以帮助你找出性能瓶颈。

    • 4.2 使用 React Native Profiler 进行性能分析

      React Native Profiler 是一个 React Native 官方提供的性能分析工具,可以帮助你找出组件渲染的性能瓶颈。

    • 4.3 避免使用 console.log

      在生产环境中,应该避免使用 console.log,因为它会影响性能。

    • 4.4 使用 Hermes 引擎

      Hermes 是 Facebook 专门为 React Native 开发的 JavaScript 引擎,它可以提高应用的启动速度和运行性能。Expo 默认使用 Hermes。

      // 在 app.json 中启用 Hermes
      {
        "expo": {
          "jsEngine": "hermes"
        }
      }

性能优化总结:

优化不是一蹴而就的,需要不断地分析和尝试。记住,好的性能是用户体验的基础。

优化方向 优化手段 备注
渲染优化 PureComponent, React.memo, useCallback, useMemo 减少不必要的渲染,缓存函数和计算结果
列表优化 FlatList, SectionList 虚拟化列表,只渲染可见元素
JavaScript 优化 Lodash, Ramda, Immutable.js 使用优化过的函数,使用不可变数据结构
网络优化 CDN, 图片压缩, Gzip, 缓存 减少网络传输时间,避免重复请求
其他优化 Chrome DevTools, React Native Profiler, Hermes 使用性能分析工具,避免使用 console.log,使用 Hermes 引擎

第二部分:原生模块桥接——打通 React Native 和原生世界的任督二脉

React Native 虽然能跨平台,但有些功能还是需要调用原生 API 才能实现,比如访问相机、蓝牙、GPS 等。这时就需要用到原生模块桥接。

  • 1. 为什么要桥接?

    React Native 的核心是 JavaScript,而原生平台(iOS 和 Android)使用不同的语言(Objective-C/Swift 和 Java/Kotlin)。桥接就是建立 JavaScript 和原生代码之间的通信桥梁,让 JavaScript 可以调用原生 API。

  • 2. 如何桥接?

    • 2.1 创建原生模块

      • iOS (Objective-C/Swift):

        创建一个 Objective-C 或 Swift 类,并使用 RCT_EXPORT_MODULE() 宏将其暴露给 JavaScript。

        // MyModule.h
        #import <React/RCTBridgeModule.h>
        
        @interface MyModule : NSObject <RCTBridgeModule>
        
        @end
        
        // MyModule.m
        #import "MyModule.h"
        #import <React/RCTLog.h>
        
        @implementation MyModule
        
        RCT_EXPORT_MODULE();
        
        RCT_EXPORT_METHOD(myNativeFunction:(NSString *)param callback:(RCTResponseSenderBlock)callback)
        {
          RCTLogInfo(@"MyModule.myNativeFunction called with param: %@", param);
          NSString *result = [NSString stringWithFormat:@"Hello from native! You sent: %@", param];
          callback(@[[NSNull null], result]);
        }
        
        @end
      • Android (Java/Kotlin):

        创建一个 Java 或 Kotlin 类,并实现 ReactContextBaseJavaModule 接口。

        // MyModule.java
        package com.example.mymodule;
        
        import com.facebook.react.bridge.ReactApplicationContext;
        import com.facebook.react.bridge.ReactContextBaseJavaModule;
        import com.facebook.react.bridge.ReactMethod;
        import com.facebook.react.bridge.Callback;
        
        public class MyModule extends ReactContextBaseJavaModule {
        
          public MyModule(ReactApplicationContext reactContext) {
            super(reactContext);
          }
        
          @Override
          public String getName() {
            return "MyModule";
          }
        
          @ReactMethod
          public void myNativeFunction(String param, Callback callback) {
            System.out.println("MyModule.myNativeFunction called with param: " + param);
            String result = "Hello from native! You sent: " + param;
            callback.invoke(null, result);
          }
        }
    • 2.2 注册原生模块

      • iOS: 不需要手动注册,React Native 会自动找到并注册原生模块。
      • Android: 需要在 MainApplication.java 中注册原生模块。

        // MainApplication.java
        import com.example.mymodule.MyModulePackage; // 导入你的包
        
        @Override
        protected List<ReactPackage> getPackages() {
          @SuppressWarnings("UnnecessaryLocalVariable")
          List<ReactPackage> packages = new PackageList(this).getPackages();
          // Packages that cannot be autolinked yet can be added manually here, for example:
          // packages.add(new MyReactNativePackage());
          packages.add(new MyModulePackage()); // 添加你的包
          return packages;
        }

        同时需要创建一个Package类,用于将Module注册到React Native中。

        // MyModulePackage.java
        package com.example.mymodule;
        
        import com.facebook.react.ReactPackage;
        import com.facebook.react.bridge.NativeModule;
        import com.facebook.react.bridge.ReactApplicationContext;
        import com.facebook.react.uimanager.ViewManager;
        
        import java.util.ArrayList;
        import java.util.Collections;
        import java.util.List;
        
        public class MyModulePackage implements ReactPackage {
            @Override
            public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
                List<NativeModule> modules = new ArrayList<>();
        
                modules.add(new MyModule(reactContext));
        
                return modules;
            }
        
            @Override
            public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
                return Collections.emptyList();
            }
        }
    • 2.3 在 JavaScript 中调用原生模块

      使用 NativeModules 对象访问原生模块。

      import { NativeModules } from 'react-native';
      
      const { MyModule } = NativeModules;
      
      const callNativeFunction = async () => {
        try {
          const result = await MyModule.myNativeFunction('Hello from JavaScript!');
          console.log('Result from native:', result);
        } catch (error) {
          console.error('Error calling native function:', error);
        }
      };
      
      callNativeFunction();
  • 3. 异步操作和回调

    原生模块中的操作通常是异步的,所以需要使用回调函数或 Promise 来处理结果。

    • 回调函数: 原生模块接收一个回调函数作为参数,并在操作完成后调用该回调函数。

      // MyModule.m (iOS)
      RCT_EXPORT_METHOD(myAsyncFunction:(NSString *)param callback:(RCTResponseSenderBlock)callback)
      {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
          // 模拟耗时操作
          sleep(2);
          NSString *result = [NSString stringWithFormat:@"Async result: %@", param];
          callback(@[[NSNull null], result]);
        });
      }
      
      // MyModule.java (Android)
      @ReactMethod
      public void myAsyncFunction(String param, Callback callback) {
        new Thread(() -> {
          try {
            Thread.sleep(2000); // 模拟耗时操作
            String result = "Async result: " + param;
            callback.invoke(null, result);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }).start();
      }
      
      // JavaScript
      const callNativeFunction = () => {
        MyModule.myAsyncFunction('Hello from JavaScript!', (error, result) => {
          if (error) {
            console.error('Error:', error);
          } else {
            console.log('Result:', result);
          }
        });
      };
    • Promise: 原生模块返回一个 Promise 对象,可以使用 async/await 语法处理结果。需要使用 RCT_REMAP_METHOD (iOS) 和 Promise (Android)

      // MyModule.h (iOS)
      #import <React/RCTBridgeModule.h>
      
      @interface MyModule : NSObject <RCTBridgeModule>
      
      @end
      
      // MyModule.m (iOS)
      #import "MyModule.h"
      #import <React/RCTLog.h>
      
      @implementation MyModule
      
      RCT_EXPORT_MODULE();
      
      RCT_REMAP_METHOD(myPromiseFunction,
                       param:(NSString *)param
                       resolver:(RCTPromiseResolveBlock)resolve
                       rejecter:(RCTPromiseRejectBlock)reject)
      {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
          // 模拟耗时操作
          sleep(2);
          NSString *result = [NSString stringWithFormat:@"Promise result: %@", param];
          resolve(result);
        });
      }
      
      @end
      
      // MyModule.java (Android)
      @ReactMethod
      public void myPromiseFunction(String param, Promise promise) {
        new Thread(() -> {
          try {
            Thread.sleep(2000); // 模拟耗时操作
            String result = "Promise result: " + param;
            promise.resolve(result);
          } catch (InterruptedException e) {
            e.printStackTrace();
            promise.reject("ERROR", "Something went wrong", e);
          }
        }).start();
      }
      
      // JavaScript
      const callNativeFunction = async () => {
        try {
          const result = await MyModule.myPromiseFunction('Hello from JavaScript!');
          console.log('Result:', result);
        } catch (error) {
          console.error('Error:', error);
        }
      };
  • 4. Expo 的 Native Modules API

    Expo 提供了一套 Native Modules API,可以让你更方便地访问原生功能,而无需编写原生代码。例如,可以使用 expo-camera 访问相机,可以使用 expo-location 访问 GPS。

    import * as Location from 'expo-location';
    
    const getLocation = async () => {
      let { status } = await Location.requestForegroundPermissionsAsync();
      if (status !== 'granted') {
        console.log('Permission to access location was denied');
        return;
      }
    
      let location = await Location.getCurrentPositionAsync({});
      console.log(location);
    };
    
    getLocation();
  • 5. 调试原生模块

    调试原生模块需要使用原生平台的调试工具,例如 Xcode (iOS) 和 Android Studio (Android)。

  • 6. 注意事项

    • 线程安全: 在原生模块中进行耗时操作时,应该使用后台线程,避免阻塞主线程。
    • 内存管理: 在原生模块中要注意内存管理,避免内存泄漏。
    • 错误处理: 在原生模块中要进行错误处理,并将错误信息传递给 JavaScript。
    • 类型转换: JavaScript 和原生代码之间的数据类型可能不同,需要进行类型转换。

原生模块桥接总结:

原生模块桥接是 React Native 开发中高级技巧,它可以让你充分利用原生平台的强大功能。掌握原生模块桥接,你的 React Native 应用将无所不能。

方面 iOS (Objective-C/Swift) Android (Java/Kotlin) 备注
模块定义 RCT_EXPORT_MODULE() ReactContextBaseJavaModule, @ReactMethod 使用宏暴露模块,实现接口
方法暴露 RCT_EXPORT_METHOD() @ReactMethod 使用宏暴露方法
异步回调 RCTResponseSenderBlock Callback, Promise 使用回调函数或 Promise 处理异步结果
Promise RCT_REMAP_METHOD, RCTPromiseResolveBlock, RCTPromiseRejectBlock Promise 使用 Promise 需要 Remap 和 Resolve/Reject
注册模块 自动 MainApplication.java, ReactPackage 需要手动注册
Expo Native Modules expo-camera, expo-location等 expo-camera, expo-location等 Expo 提供了一套 Native Modules API,简化原生功能访问

好了,今天的讲座就到这里。希望大家能有所收获,把自己的 React Native 应用打磨得更加完美! 咱们下回再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注