编写 Android 原生模块的若干提示

假设你已经阅读了官方文档 Android Native Modulesopen in new window,并且已经学会如何编写一个简单的 Android 原生模块。

使用脚手架创建原生模块

可以使用 react-native-create-libopen in new window 来创建原生模块

例如:

npx react-native-create-lib  \
  --module-name @sdcx/wechat \
  --repo-name react-native-wechat \
  wechat

原生模块必须具有类型定义

没有类型的代码是难以维护的。我们要求为导出的原生模块声明完整的类型。

这些类型定义同时作为接口和协议存在,方便多端同事协作,不管是编写原生模块,还是使用原生模块。

import { NativeModules, NativeModule } from "react-native"

interface PayInfo {
  partnerId: string
  prepayId: string
  nonceStr: string
  timeStamp: number
  sign: string
  package: string
}

interface WechatModule extends NativeModule {
  registerApp(appId: string, universalLink: string): Promise<boolean>
  isWXAppInstalled(): Promise<boolean>
  sendAuthRequest(): Promise<string>
  pay(info: PayInfo): Promise<string>
}

const WechatModule: WechatModule = NativeModules.WechatModule

export default WechatModule

不要直接导出原生模块给到用户

我们不能把原生模块直接暴露给用户,而是先要包裹一下。

如果模块比较简单,可以将原生模块中的每个方法分别包裹为函数,然后以命名空间的形式导出。

// wechat.ts
import { NativeModules } from "react-native"
const WechatModule: WechatModule = NativeModules.WechatModule

export function registerApp(appId: string, universalLink: string): Promise<boolean> {
  return WechatModule.registerApp(appId, universalLink)
}

export function isWXAppInstalled(): Promise<boolean> {
  return WechatModule.isWXAppInstalled()
}

export function sendAuthRequest(): Promise<string> {
  return WechatModule.sendAuthRequest()
}
// index.ts
export * as WechatSDK from "./wechat"

如果模块比较复杂,需要在 React 侧做一些逻辑处理,那么可以将原生模块包裹到一个对象中导出。

// index.ts
import { NativeModules } from "react-native"
const WechatModule: WechatModule = NativeModules.WechatModule

const WechatSDK = {
  registerApp: function (appId: string, universalLink: string) {
    return WechatModule.registerApp(appId, universalLink)
  },

  isWXAppInstalled: function () {
    return WechatModule.isWXAppInstalled()
  },

  sendAuthRequest: function () {
    return WechatModule.sendAuthRequest()
  },
}

export default WechatSDK

转换 ReadableMap 参数为模型

在处理 React 侧传递过来的参数时,将 ReadableMap 转换为原生模型,是个较好的模式。

@ReactMethod
public void launchCamera(final ReadableMap options, final Callback callback) {
    this.options = new Options(options);
}
public class Options {
    Boolean pickVideo = false;
    Boolean includeBase64;
    int videoQuality = 1;
    int quality;

    Options(ReadableMap options) {
        if (options.getString("mediaType").equals("video")) {
            pickVideo = true;
        }
        includeBase64 = options.getBoolean("includeBase64");

        String videoQualityString = options.getString("videoQuality");
        if(!TextUtils.isEmpty(videoQualityString) && !videoQualityString.toLowerCase().equals("high")) {
            videoQuality = 0;
        }

        quality = (int) (options.getDouble("quality") * 100);
    }
}

谨慎使用 Arguments.toBundle()

某些时候,可能需要使用 Arguments.toBundle() 将 React Native 传递过来的数据转换成 Bundle。

不过此时务必小心:所有的 number 都被转换成了 double

@ReactMethod
public void pay(@Nonnull ReadableMap data, Promise promise) {
    Bundle bundle = Arguments.toBundle(data);
    int count = (int)bundle.getDouble("count");
}

总是在主线程操作 UI

可以使用 UiThreadUtil 检测当前是否主线程,调度任务到主线程执行

@ReactMethod
public void isWXAppInstalled(Promise promise) {
    UiThreadUtil.runOnUiThread(() -> wechatHelper.isWXAppInstalled(promise));
}

JavaModule 是个门面

这里的 JavaModule 是指继承了 ReactContextBaseJavaModule 的类

说这种类是个门面,有两层意思。

其一,JavaModule 是连接 Native 和 React 两个世界的桥梁,只起连接作用,真正干活的另有其人。

譬如下面这个微信 SDK 例子,真正的活被转发给了 wechatHelper,原生模块负责线程切换。

// WechatModule.java
@ReactMethod
public void pay(@Nonnull ReadableMap data, Promise promise) {
    UiThreadUtil.runOnUiThread(() -> wechatHelper.pay(data, promise));
}

其二,如果原生代码需要用到的功能和原生模块相关,那么由 JavaModule 负责转发。

还是微信 SDK 的例子,我们可以在 WechatModule 内部暴露一个静态方法供原生代码使用,而不直接暴露 wechatHelper,否则,我们可能需要将 wechatHelper 做成单例。

public void handleIntent(Intent intent) {
    wechatHelper.handleIntent(intent);
}

public static void handleIntent(@Nullable ReactContext reactContext, Intent intent) {
    if (reactContext != null && reactContext.hasActiveCatalystInstance()) {
        WechatModule wechatModule = reactContext.getNativeModule(WechatModule.class);
        if (wechatModule != null) {
            wechatModule.handleIntent(intent);
        }
    }
}

然后我们在 WXEntryActivityWXPayEntryActivity 中可以通过如下方式调用 handleIntent

@Override
protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    setIntent(intent);
    WechatModule.handleIntent(MainApplication.get().getCurrentReactContext(), getIntent());
    finish();
}

与 React 侧通信前,先检测 ReactContext 是否存活

譬如向 React 侧发送事件时

public void sendEvent(@NonNull String eventName, @NonNull WritableMap params) {
    if (reactContext.hasActiveCatalystInstance())) {
        reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                .emit(eventName, params);
    }
}

当 ReactContext 被销毁时,释放相关资源

如果模块持有资源,那么在 onCatalystInstanceDestroy 中释放它们。

// WechatModule.java
@Override
public void onCatalystInstanceDestroy() {
    super.onCatalystInstanceDestroy();
    this.wechatHelper.handleReload();
}
// WechatHelper.java
public void handleReload() {
    if (broadcastReceiver != null) {
        context.unregisterReceiver(broadcastReceiver);
    }
}

发生错误时,总是返回 code 和 message

记住,code 是个字符串。

我们遵循这么一个原则:code 用来进一步分析问题,message 则可以直接提示给用户。

promise.reject(errorCode(resp.errCode), resp.errStr);
private String errorCode(int errCode) {
    Map<Integer, String> map = new HashMap<>();
    map.put(BaseResp.ErrCode.ERR_OK, "WXSuccess");
    map.put(BaseResp.ErrCode.ERR_COMM, "WXErrCodeCommon");
    map.put(BaseResp.ErrCode.ERR_USER_CANCEL, "WXErrCodeUserCancel");
    map.put(BaseResp.ErrCode.ERR_SENT_FAILED, "WXErrCodeSentFail");
    map.put(BaseResp.ErrCode.ERR_AUTH_DENIED, "WXErrCodeAuthDeny");
    map.put(BaseResp.ErrCode.ERR_UNSUPPORT, "WXErrCodeUnsupport");
    map.put(BaseResp.ErrCode.ERR_BAN, "WXErrCodeBan");
    return map.get(errCode);
}

在 React 侧,可以通过以下方式获取 code 和 message

try {
  // ...
} catch (error) {
  console.log(error.code, error.message)
}

正确使用事件

和回调或 Promise 只能生效一次不同,事件可以源源不断地从原生侧传递给 React 侧。

在 Native 侧

通过 getConstants 导出事件名为常量

public static final String WECHAT_STAFF_EVENT = "WECHAT_STAFF_EVENT";

@Nullable
@Override
public Map<String, Object> getConstants() {
    final Map<String, Object> constants = new HashMap<>();
    constants.put("WECHAT_STAFF_EVENT", WECHAT_STAFF_EVENT);
    // ...
    return constants;
}

像如下那样发布事件

public void sendEvent(@NonNull String eventName, @NonNull WritableMap params) {
    if (reactContext.hasActiveCatalystInstance())) {
        reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                .emit(eventName, params);
    }
}

在 React 侧,使用 NativeEventEmitter 包裹原生模块,得到一个 EventEmitter 实例

import { NativeModules, NativeModule, NativeEventEmitter } from "react-native"

interface WechatModule extends NativeModule {
  WECHAT_STAFF_EVENT: string
}

const WechatModule: WechatModule = NativeModules.WechatModule

const WechatEventEmitter: NativeEventEmitter = new NativeEventEmitter(WechatModule)

export { WechatEventEmitter, WechatModule }

像如下那样监听事件

useEffect(() => {
  const subscription = WechatEventEmitter.addListener(WechatModule.WECHAT_STAFF_EVENT, () => {
    // do something
  })
  return () => {
    subscription.remove()
  }
})

正确处理需要系统权限的操作

某些涉及隐私的操作,需要用户授予权限才能进行。譬如获取用户位置,访问相册等等。

我们遵循职责分离的原则,原生模块不负责请求权限,仅在执行操作前,检查是否拥有执行操作所需权限,如果没有,则通过 Error 机制把异常信息返回给 React 侧,由 React 侧进行处理。

@ReactMethod
public void launchCamera(final ReadableMap options, final Promise promise) {
    if (!isCameraPermissionFulfilled(reactContext, currentActivity)) {
        promise.reject('CameraPermisseionError', '缺少相机权限')
        return;
    }
}

在 React 侧,我们使用 react-native-permissionsopen in new window 处理权限的检查和请求。

通常会使用高阶函数,把需要权限的请求包裹一下。

// PermissionUtil.ts
export function withCameraPermission<T>(fn: () => Promise<T>) {
  if (Platform.OS === "ios") {
    return withCameraPermissionIOS(fn)
  } else {
    return withCameraPermissionAndroid(fn)
  }
}
// App.tsx
const handleAvatarPress = async () => {
  const result = await withCameraPermission(pickImageFromCamera)()
}

将服务鉴权委托给主工程

有时,我们封装的原生组件库,可能需要访问我们自己的服务,而这些服务需要鉴权。

我们的组件库是集成到我们的主工程中使用的,而我们的主工程已经拥有一套鉴权机制来获取 token,刷新 token。

我们的组件库应该复用主工程这套鉴权机制。

可我们的组件库又不能依赖我们的主工程,该怎么办呢?

答案是控制反转。我们在组件库声明一套鉴权接口,由主工程注入实现。

// 组件库
export interface TokenProvider {
  refreshToken: () => Promise<string>
  getAccessToken: () => Promise<string>
}

const Track = {
  setTokenProvider: function (tokenProvider: TokenProvider) {
    _tokenProvider = tokenProvider
  },
}
// 主工程
Track.setTokenProvider({
  refreshToken: AuthService.refreshToken,
  getAccessToken: AuthService.getAccessToken,
})

不仅仅是服务鉴权,任何组件库不知道怎么办,或不是它的职责的事情,都可以通过控制反转委托给主工程来处理。

避免 onActivityResult 陷阱

有时,我们需要启动其它 Activity 并通过 onActivityResult 来接收其返回来的结果。

此时要注意 requestCode 不要和其它组件库有所冲突,并且如果 requestCode 不属于本组件库,要尽早返回。

如下所示

@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
    if (!isValidRequestCode(requestCode)) {
        return;
    }
}

合理使用请求-响应模式

某些请求是异步的,并需要支持并发,此时,可以使用请求-响应模式。

譬如下面这个例子,我们需要通过地理坐标来获取地区编码

// LocationModule.java
@ReactMethod
public void adcode(double lat, double lon, final Promise promise) {
    AdcodeRequest adcodeRequest = new AdcodeRequest(getReactApplicationContext());
    adcodeRequest.execute(lat, lon, new AdcodeRequest.AdcodeRequestListener() {
        @Override
        public void onSuccess(@Nonnull String adcode) {
            promise.resolve(adcode);
        }

        @Override
        public void onFailure(@Nonnull String errorInfo) {
            promise.reject(ErrorCode, errorInfo);
        }
    });
}
// AdcodeRequest.java
public class AdcodeRequest {
    public interface AdcodeRequestListener {
        void onSuccess(@Nonnull String adcode);
        void onFailure(@Nonnull String errorInfo);
    }

    private final Context mContext;
    public AdcodeRequest(Context context) {
        mContext = context;
    }

    public void execute(double lat, double lon, @Nonnull AdcodeRequestListener listener) {
        RegeocodeQuery query = new RegeocodeQuery(new LatLonPoint(lat, lon), 200f, GeocodeSearch.AMAP);
        GeocodeSearch geocodeSearch = new GeocodeSearch(mContext.getApplicationContext());
        geocodeSearch.setOnGeocodeSearchListener(new GeocodeSearch.OnGeocodeSearchListener() {
            @Override
            public void onRegeocodeSearched(RegeocodeResult result, int code) {
                if (code == 1000) {
                    String adCode = result.getRegeocodeAddress().getAdCode();
                    listener.onSuccess(adCode);
                } else {
                    listener.onFailure("未能获得地理编码:" + code);
                }
            }

            @Override
            public void onGeocodeSearched(GeocodeResult geocodeResult, int code) {

            }
        });

        geocodeSearch.getFromLocationAsyn(query);
    }
}

别忘了混淆规则

如果引用了第三方库,别忘了在组件库的 proguard-rules.pro 文件中设置混淆规则。

上次更新: 6/30/2022, 6:52:25 AM