React Native中 Back 键的攻坚实战

958 查看

前言

在ReactNative中,因为在Native层只有一个MainActivity来承载Js层的逻辑,因此Android上的Back键会在点击一次后直接退出应用。

Detect hardware back button presses, and programmatically invoke the default back button functionality to exit the app if there are no listeners or if none of the listeners return true.
——Facebook offical

Docs from facebook

BackAndroid.addEventListener('hardwareBackPress', function() { 
  if (!this.onMainScreen()) {
    this.goBack(); return true; // 执行岀栈操作
  } 
  return false; // 退出app
});

Method:
static exitApp()
static addEventListener(eventName, handler)
static removeEventListener(eventName, handler)

如上是我们能够在官方文档所能找到的资料。看完后是不是有种不知所措的懵比?玛德onMainScreen 是什么鬼?goBack又是什么鬼?在哪儿定义?WTF!!!

BackAndroid源码解析

exitApp:

  exitApp: function() {
    DeviceEventManager.invokeDefaultBackPressHandler();
  }

好吧,就一行代码,那我们继续深入invokeDefaultBackPressHandler
我们来到了DeviceEventManagerModule.java里,看到了如下:

/**
   * Invokes the default back handler for the host of this catalyst instance. This should be invoked
   * if JS does not want to handle the back press itself.
   */
  @ReactMethod
  public void invokeDefaultBackPressHandler() {
    getReactApplicationContext().runOnUiQueueThread(mInvokeDefaultBackPressRunnable);
  }

可以看到这里类似于我们在Native中跳回UI线程的做法,这里最终在Runnable对象的run方法里执行的是invokeDefaultOnBackPressed方法,最终跟踪到Native层后,发现调用的是ReactActivity( Native层中MainActivity的父类,也就是实现了与Js层通信的核心类)中的super.onBackPress()这个方法。具体React Native和Native的通信方式我们在这里不作为重点讲解(毕竟道行不够深,乱讲容易误人子弟)

可以看出,这里的exitApp()方法就是调用了Native层的onBackPress方法,这也验证了文章开头讲的部分内容。

addEventListener
removeEventListener:

  addEventListener: function (
    eventName: BackPressEventName,
    handler: Function
  ): {remove: () => void} {
    _backPressSubscriptions.add(handler);
    return {
      remove: () => BackAndroid.removeEventListener(eventName, handler),
    };
  },

  removeEventListener: function(
    eventName: BackPressEventName,
    handler: Function
  ): void {
    _backPressSubscriptions.delete(handler);
  },

抛开方法签名,直接看方法体:

  • 在addEventListener中,在给_backPressSubscriptions这个集合添加我们的回调方法(稍后仔细看看这部分逻辑)后,直接利用闭包返回了移除监听事件的方法。

  • 在removeEvent中,也只是_backPressSubscription中这个集合删除了对应的这个方法。从这里可以看到,一定要保证handler这个处理回调的唯一性,否则就会导致remove失败。

  • 关于_backPressSubscription:

var DeviceEventManager = require('NativeModules').DeviceEventManager;
var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');

var DEVICE_BACK_EVENT = 'hardwareBackPress';

type BackPressEventName = $Enum<{
  backPress: string;
}>;

var _backPressSubscriptions = new Set();

RCTDeviceEventEmitter.addListener(DEVICE_BACK_EVENT, function() {
  var backPressSubscriptions = new Set(_backPressSubscriptions);
  var invokeDefault = true;
  backPressSubscriptions.forEach((subscription) => {
    if (subscription()) {
      invokeDefault = false;
    }
  });
  if (invokeDefault) {
    BackAndroid.exitApp();
  }
});

这里主要使用了RCTDeviceEventEmitter(ReactNative中观察者模式的一种实现)来实现具体的响应逻辑。这里的addListener是观察者(订阅者),而被观察者应该就是Back键被触发后的响应逻辑(这部分应该由底层实现)。

这里维护了一个Set集合,然后在事件订阅函数里依次对订阅者按照相应顺序进行响应处理。
里边还有一个标记量invokeDefault,默认值给了true。在使用forEach遍历完回调的过程中,如果invokeDefault因为回调的返回值为真值时被置为false, 那么在执行完毕后将不执行exitApp的操作(这也是在某种程度上保护了一些页面无辜被退出应用的场景)
综上所述,onMainScreen、goBack只是一个概念化的方法,仅用来告诉你该干什么

实战使用BackAndroid

 constructor (props) {
    super(props)
    this.handleBack = this._handleBack.bind(this) // 返回一个绑定好this的方法并存储于当前实例中
  }

  componentDidMount () {
    BackAndroid.addEventListener('hardwareBackPress', this.handleBack)
  }

  componentWillUnmount () {
    BackAndroid.removeEventListener('hardwareBackPress', this.handleBack)
  }

  _handleBack () {
    var navigator = this.navigator

    if (navigator && navigator.getCurrentRoutes().length > 1) {
      navigator.pop()
      return true
    }
    return false
  }

订阅与反订阅

以上是之前写的一个例子,其中坑也不少:

  • 在订阅和反订阅的时候,一定要保证handleBack的唯一性,这牵扯到能否反订阅成功
  • 因为这一对儿函数是高阶函数,因此这里的写法目前有三种:
    • Lambda 表达式
    • 匿名函数
    • this._handleBack.bind(this) 绑定this的方式来返回一个含有this的纯函数
      但是以上三种方式都有一个问题,就是每次调用返回的都是一个全新的函数对象,这样明显保证不了函数的唯一性,因此有了构造方法里的这个函数this.handleBack = this._handleBack.bind(this)

上边也提到了,在订阅的时候,返回了一个反订阅函数(和Redux的subscribe类似),因此我们在componentDidMount里还可以这么写:

this.removeListener = BackAndroid.addEventListener('hardwareBackPress', this._handleBack.bind(this))

componentWillUnmount里就变成:

this.removeListener()

_handleBack()

  _handleBack () {
    var navigator = this.navigator

    if (navigator && navigator.getCurrentRoutes().length > 1) {
      navigator.pop()
      return true
    }
    return false
  }

这部分比较简单,逻辑基本是按照文档给的写。对navigator操作来判断是否在MainScreen以及具体的pop操作。

External

如果你想要自己监听处理Back键,也可以摆脱BackAndroid的限制,直接采用

RCTDeviceEventEmitter.addListener(‘hardwareBackPress’, function() {
  ... 
})

自己来处理Back逻辑。

士为知己者死,
女为悦己者容,
吾其报智氏之雠矣
——《战国策》
希望大家能够早日找到红颜、知己!!!

号外!!!

我司举办的开发大赛,欢迎参加喔~



详情请见官网: https://applean.cn