Android React Native使用原生模块

标签: AndroidReactNative原生模块回调
13248人阅读 评论(7) 收藏 举报
分类:

有时候我们的App需要访问平台API,并且React Native可能还没有相应的模块包装;或者你需要复用一些Java代码,而不是用Javascript重新实现一遍;又或者你需要实现某些高性能的、多线程的代码,譬如图片处理、数据库、或者各种高级扩展等等。
而用React Native可以在它的基础上编写真正原生的代码,并且可以访问平台所有的能力。如果React Native还不支持某个你需要的原生特性,你应当可以自己实现该特性的封装。

不过在开始编写代码使用原生模块前,有一个知识点需要掌握,免得又坑进去了。

在使用React Native的时候,经常会看到这么一段代码

var React = require('react-native');

那么require这个语句的作用到底是什么呢,下面的流程提取自require() 源码解读

当遇到 require(X) 时,按下面的顺序处理。
(1)如果 X 是内置模块(比如 require(‘http’))
  a. 返回该模块。
  b. 不再继续执行。
(2)如果 X 以 “./” 或者 “/” 或者 “../” 开头
a. 根据 X 所在的父模块,确定 X 的绝对路径。
b. 将 X 当成文件,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。

  • X
  • X.js
  • X.json
  • X.node

c. 将 X 当成目录,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。

  • X/package.json(main字段)
  • X/index.js
  • X/index.json
  • X/index.node

(3)如果 X 不带路径
  a. 根据 X 所在的父模块,确定 X 可能的安装目录。
  b. 依次在每个目录中,将 X 当成文件名或目录名加载。
(4) 抛出 “not found”

以上就是require语句的整个执行过程。那么require(‘react-native’);请求的到底是什么呢,其实就是node_modules\react-native\Libraries\react-native\react-native.js这个文件,该文件中导出了一些常用的组件,其源码如下

/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @flow
 */
'use strict';

// Export React, plus some native additions.
//
// The use of Object.create/assign is to work around a Flow bug (#6560135).
// Once that is fixed, change this back to
//
//   var ReactNative = {...require('React'), /* additions */}
//
var ReactNative = Object.assign(Object.create(require('React')), {
  // Components
  ActivityIndicatorIOS: require('ActivityIndicatorIOS'),
  DatePickerIOS: require('DatePickerIOS'),
  DrawerLayoutAndroid: require('DrawerLayoutAndroid'),
  Image: require('Image'),
  ListView: require('ListView'),
  MapView: require('MapView'),
  Modal: require('Modal'),
  Navigator: require('Navigator'),
  NavigatorIOS: require('NavigatorIOS'),
  PickerIOS: require('PickerIOS'),
  ProgressBarAndroid: require('ProgressBarAndroid'),
  ProgressViewIOS: require('ProgressViewIOS'),
  ScrollView: require('ScrollView'),
  SegmentedControlIOS: require('SegmentedControlIOS'),
  SliderIOS: require('SliderIOS'),
  SnapshotViewIOS: require('SnapshotViewIOS'),
  Switch: require('Switch'),
  SwitchAndroid: require('SwitchAndroid'),
  SwitchIOS: require('SwitchIOS'),
  TabBarIOS: require('TabBarIOS'),
  Text: require('Text'),
  TextInput: require('TextInput'),
  ToastAndroid: require('ToastAndroid'),
  ToolbarAndroid: require('ToolbarAndroid'),
  TouchableHighlight: require('TouchableHighlight'),
  TouchableNativeFeedback: require('TouchableNativeFeedback'),
  TouchableOpacity: require('TouchableOpacity'),
  TouchableWithoutFeedback: require('TouchableWithoutFeedback'),
  View: require('View'),
  ViewPagerAndroid: require('ViewPagerAndroid'),
  WebView: require('WebView'),

  // APIs
  ActionSheetIOS: require('ActionSheetIOS'),
  AdSupportIOS: require('AdSupportIOS'),
  AlertIOS: require('AlertIOS'),
  Animated: require('Animated'),
  AppRegistry: require('AppRegistry'),
  AppStateIOS: require('AppStateIOS'),
  AsyncStorage: require('AsyncStorage'),
  BackAndroid: require('BackAndroid'),
  CameraRoll: require('CameraRoll'),
  Dimensions: require('Dimensions'),
  Easing: require('Easing'),
  ImagePickerIOS: require('ImagePickerIOS'),
  InteractionManager: require('InteractionManager'),
  LayoutAnimation: require('LayoutAnimation'),
  LinkingIOS: require('LinkingIOS'),
  NetInfo: require('NetInfo'),
  PanResponder: require('PanResponder'),
  PixelRatio: require('PixelRatio'),
  PushNotificationIOS: require('PushNotificationIOS'),
  Settings: require('Settings'),
  StatusBarIOS: require('StatusBarIOS'),
  StyleSheet: require('StyleSheet'),
  VibrationIOS: require('VibrationIOS'),

  // Plugins
  DeviceEventEmitter: require('RCTDeviceEventEmitter'),
  NativeAppEventEmitter: require('RCTNativeAppEventEmitter'),
  NativeModules: require('NativeModules'),
  Platform: require('Platform'),
  processColor: require('processColor'),
  requireNativeComponent: require('requireNativeComponent'),

  // Prop Types
  EdgeInsetsPropType: require('EdgeInsetsPropType'),
  PointPropType: require('PointPropType'),

  // See http://facebook.github.io/react/docs/addons.html
  addons: {
    LinkedStateMixin: require('LinkedStateMixin'),
    Perf: undefined,
    PureRenderMixin: require('ReactComponentWithPureRenderMixin'),
    TestModule: require('NativeModules').TestModule,
    TestUtils: undefined,
    batchedUpdates: require('ReactUpdates').batchedUpdates,
    cloneWithProps: require('cloneWithProps'),
    createFragment: require('ReactFragment').create,
    update: require('update'),
  },
});

if (__DEV__) {
  ReactNative.addons.Perf = require('ReactDefaultPerf');
  ReactNative.addons.TestUtils = require('ReactTestUtils');
}

module.exports = ReactNative;

了解了这个知识点后,我们来自定义一个模块,去使用原生的模块。假设有这么一个需求,我们需要使用Andorid中的Log类,但是React Native并没有为我们进行封装,那么我们自己动手实现一下吧。

  • 我们需要继承ReactContextBaseJavaModule这个抽象类,重写getName()函数,用于返回一个字符串,这个字符串在JavaScript端标记这个模块,暴露一个函数给javascript端,并且使用注解@ReactMethod进行标记,该函数的返回值必须为void,React Native的跨语言访问是异步进行的,所以想要给JavaScript返回一个值的唯一办法是使用回调函数或者发送事件
  • 我们需要实现一个类实现ReactPackage接口,该接口中有三个抽象函数待实现,分别是createNativeModulescreateJSModulescreateViewManagers,这三个函数中,我们需要实现的最关键的函数就是createNativeModules,在该函数中我们需要添加前一步创建的ReactContextBaseJavaModule子类
  • 构建ReactInstanceManager的实例时,通过调用 addPackage()函数,将上一步实现的ReactPackage添加进去。

接下来我们来实现代码。为了简单方便,这里只演示Log类中的d方法,即Log.d(String tag,String msg)

第一步,继承ReactContextBaseJavaModule类,重写getName()方法,因为是Log模块,所以直接返回字符串Log,暴露一个d方法给javascript端,返回值为void,只用注解进行标记。最终的代码如下。

public class LogModule extends ReactContextBaseJavaModule{
    private static final String MODULE_NAME="Log";

    public LogModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Override
    public String getName() {
        return MODULE_NAME;
    }

    @ReactMethod
    public void d(String tag,String msg){
        Log.d(tag,msg);
    }
}

第二步,实现ReactPackage接口,在createNativeModules函数中添加我们的日志模块。其余两个函数返回空的List即可。

public class AppReactPackage implements ReactPackage {
    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        List<NativeModule> modules=new ArrayList<>();
        modules.add(new LogModule(reactContext));
        return modules;
    }

    @Override
    public List<Class<? extends JavaScriptModule>> createJSModules() {
        return Collections.emptyList();
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }
}

第三步,添加AppReactPackage 到ReactInstanceManager的实例中去,在我们的MainActivity中可以看到这么一段代码

mReactInstanceManager = ReactInstanceManager.builder()
                .setApplication(getApplication())
                .setBundleAssetName("index.android.bundle")
                .setJSMainModuleName("index.android")
                .addPackage(new MainReactPackage())
                .setUseDeveloperSupport(BuildConfig.DEBUG)
                 .setInitialLifecycleState(LifecycleState.RESUMED)
                .build();

我们再build函数之前调用addPackage进行添加即可,最终代码如下。

 mReactInstanceManager = ReactInstanceManager.builder()
                .setApplication(getApplication())
                .setBundleAssetName("index.android.bundle")
                .setJSMainModuleName("index.android")
                .addPackage(new MainReactPackage())
                .addPackage(new AppReactPackage())
                .setUseDeveloperSupport(BuildConfig.DEBUG)
                 .setInitialLifecycleState(LifecycleState.RESUMED)
                .build();

可以看到我们增加了一行.addPackage(new AppReactPackage())

这样,在Java端我们要做的就做完了,接下来就是javascript端了,这时候编译一下apk重新后运行,接下来我们来编写javascript端。

如果你不嫌麻烦,每次都要从NativeModules来访问我们的Log,那么现在你可以直接在javascript中进行访问了。就像这样子。


var React = require('react-native');

var {
  NativeModules,
} = React;

var Log1= NativeModules.Log;

Log1.d("Log1","LOG");

但是,假如我再增加一个需求,就是当Log类在java层打印出一个日志的之后,希望在js端也输出以下这个日志,那么你会怎么做呢,或许你会说,这个简单,我再输出一下js的日志就ok了。就像这样子。


var React = require('react-native');

var {
  NativeModules,
} = React;

var Log1= NativeModules.Log;

Log1.d("Log1","LOG");
console("Log1","LOG");

没错是没错,就是看着蛋疼,不好维护不说,通样的代码你得写多少遍。

这时候,我们就有必要封装一下javascript端的代码了,在index.android.js文件同目录下新建一个log.js,输入如下代码。

'use strict';
var { NativeModules } = require('react-native');

var RCTLog= NativeModules.Log;

var Log = {
  d: function (
    tag: string,
    msg: string
  ): void {
    console.log(tag,msg);
    RCTLog.d(tag, msg);
  },
};

module.exports = Log;

代码很简单,我们通过NativeModules拿到我们的Log模块在本地的实现,赋值给变量RCTLog,并且还声明了一个Log变量,里面有一个函数d,调用了RCTLog的d函数,并且在调用前输出了javascript端的日志。最后使用module.exports=Log将Log变量导出

上面我们规定了参数类型为string,java与javascript之间参数类型的对应关系如下

Boolean -> Bool
Integer -> Number
Double -> Number
Float -> Number
String -> String
Callback -> function
ReadableMap -> Object
ReadableArray -> Array

接下来就是引用log.js文件了,看过上面的require语句的解析,这对你应该不成问题了。

var Log=require('./log');
Log.d("TAG","111");

这还没完,我们再提一个需求,就是我们希望这个Log模块能够提供一个常量,也就是TAG供我们使用,而这个常量定义在java层,以便以后我们使用的时候如果不想输入TAG,可以直接使用这个默认的TAG,就像这样子

var Log=require('./log');
Log.d(Log.TAG,"111");

那么这个要怎么实现呢,很显然,我们需要在log.js中加入这个变量,就像这样子

'use strict';


var { NativeModules } = require('react-native');

var RCTLog= NativeModules.Log;
var Log = {
  TAG: RCTLog.TAG,
  d: function (
    tag: string,
    msg: string
  ): void {
    console.log(tag,msg);
    RCTLog.d(tag, msg);
  },
};

module.exports = Log;

这样虽然我们可以使用Log.TAG返回到这个值了,由于我们java层没有定义TAG,所以这时候会报错。因此我们需要在java层返回这个值,这又要怎么做呢,别急。我们重新回过头来看看我们实现的类LogModule,我们继续在该类中定义两个常量

private static final String TAG_KEY = "TAG";
private static final String TAG_VALUE = "LogModule";

什么用呢,看常量名字就是到了,key和value,键值对,我们希望通过TAG_KEY拿到TAG_VALUE ,也就是我们日志要用到的TAG,怎么实现呢。重写getConstants函数即可。

  @Override
    public Map<String, Object> getConstants() {
        final Map<String, Object> constants = MapBuilder.newHashMap();
        constants.put(TAG_KEY, TAG_VALUE);
        return constants;
    }

这时候重写编译运行一下,你就可以在javascript层通过Log.TAG就可以访问到对应的值了,值为LogModule,而为什么是Log.TAG而不是其他的值呢,因为我们constants中put进去的键就是TAG。

那么这个有什么作用呢,还记得android中我们的Toast的使用,显示时间有两个值吗,一个是Toast.LENGTH_SHORT,另一个是Toast.LENGTH_LONG,我们希望在javascript层通用有这么两个常量可以使用,那么就可以使用这种方法。我们可以看看系统的ToastAndroid的实现。

首先看java层

public class ToastModule extends ReactContextBaseJavaModule {

  private static final String DURATION_SHORT_KEY = "SHORT";
  private static final String DURATION_LONG_KEY = "LONG";

  public ToastModule(ReactApplicationContext reactContext) {
    super(reactContext);
  }

  @Override
  public String getName() {
    return "ToastAndroid";
  }

  @Override
  public Map<String, Object> getConstants() {
    final Map<String, Object> constants = MapBuilder.newHashMap();
    constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT);
    constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG);
    return constants;
  }

  @ReactMethod
  public void show(String message, int duration) {
    Toast.makeText(getReactApplicationContext(), message, duration).show();
  }
}

看到没有,在getConstants函数中暴露了两个值给javascript层,SHORT对应java层的Toast.LENGTH_SHORT,LONG对应java层的Toast.LENGTH_LONG。接着看javascript层的代码

'use strict';

var RCTToastAndroid = require('NativeModules').ToastAndroid;

var ToastAndroid = {

  SHORT: RCTToastAndroid.SHORT,
  LONG: RCTToastAndroid.LONG,

  show: function (
    message: string,
    duration: number
  ): void {
    RCTToastAndroid.show(message+"lizhangqu", duration);
  },

};

module.exports = ToastAndroid;

直接可以通过定义的变量SHORT或者LONG访问到,最终我们的使用就是这样子的。

var React = require('react-native');
var {
  ToastAndroid
} = React;
ToastAndroid.show("toast",ToastAndroid.SHORT);

这还没完,这仅仅是没有返回值的情况,假如有返回值情况又是怎么样呢,比如javascript调用java层的方法,但是java层需要将结果返回javascript,没错,答案就是回调!,最典型的一个场景就是javascript层调用java层的网络请求方法,java层拿到网络数据后需要将结果返回给javascript层。通用的,我们用最快的速度实现一下这个模块。

继承ReactContextBaseJavaModule,实现getName方法,返回值为Net,暴露一个getResult方法给javascript,并进行注解,注意这个函数有一个Callback类型的入参,返回结果就是通过这个进行回调

public class NetModule extends ReactContextBaseJavaModule {
    private static final String MODULE_NAME="Net";
    public NetModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Override
    public String getName() {
        return MODULE_NAME;
    }

    @ReactMethod
    public void getResult(String url,final Callback callback){
        Log.e("TAG","正在请求数据");
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {

                    String result="这是结果";
                    Thread.sleep(1000);//模拟网络请求
                    callback.invoke(true,result);
                } catch (Exception e) {
                    e.printStackTrace();
                }

            }
        }).start();


    }
}

Callback的定义如下,它是一个接口,invoke函数的入参是个数是任意的。

public interface Callback {

  /**
   * Schedule javascript function execution represented by this {@link Callback} instance
   *
   * @param args arguments passed to javascript callback method via bridge
   */
  public void invoke(Object... args);

}

在前面的AppReactPackage类createNativeModules函数中注册该模块

modules.add(new NetModule(reactContext));

之后新建一个net.js文件,实现javascript层

'use strict';
var { NativeModules } = require('react-native');

var RCTNet= NativeModules.Net;

var Net = {

  getResult: function (
    url: string,
    callback:Function,
  ): void {
    RCTNet.getResult(url,callback);
  },
};

module.exports = Net;

进行使用

var Net=require('./net');
Net.getResult(
    "http://baidu.com",
     (code,result)=>{
        console.log("callback",code,result);
     }
);

如果不出意外,在java层将输出日志

11-20 22:30:53.598 25323-1478/com.awesomeproject E/TAG: 正在请求数据

在javascript层,控制台将输出

callback true 这是结果

以上就是回调的一个示例,你可以简单想象成java层的网络请求模型,主线程开启子线程请求数据,子线程拿到数据后回调对应的方法使用handler通知主线程返回结果。

除了回调之外,还可以使用事件调用javascript层的方法,我们以第一个LogModule为例,假设调用了java层方法后,我们希望发送一个事件给javascript,在javascript层再次进行打印输出。则d方法修改下面的代码

 @ReactMethod
    public void d(String tag,String msg){
        Log.d(tag, msg);
        //发送事件给javascript层
        WritableMap params = Arguments.createMap();
        params.putString("TAG",tag);
        params.putString("MSG",msg);
        getReactApplicationContext()
                .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                .emit("logInConsole", params);//对应的javascript层的事件名为logInConsole,注册该事件即可进行回调

    }

而对应的log.js并不需要做什么修改,我们只需要在想获得该事件的地方注册事件即可,比如我们想在主界面接收该事件,则在index.android.js中进行注册,注册的方式有两种。

第一种如下

'use strict';

var React = require('react-native');

var {
  AppRegistry,
  StyleSheet,
  View,
  DeviceEventEmitter,
} = React;


//注意下面两个模块的引入
var Log=require('./log');
var Subscribable = require('Subscribable');

var AwesomeProject = React.createClass({
  mixins: [Subscribable.Mixin],//这句必须要加
  render: function() {
    return (
    <View style={styles.container}>
   </View>
    );
  },
  componentDidMount:function(){
    Log.d("tag","tag");//调用java层log方法,之后就会回调对应的事件(前提是注册了事件)
    //注册事件
    this.addListenerOn(DeviceEventEmitter,
                       'logInConsole',
                       this.logInConsole);
  },
  logInConsole:function(event){
    console.log(event);
  },
});

var styles = StyleSheet.create({
  container:{

  },
});


AppRegistry.registerComponent('AwesomeProject', () => AwesomeProject);

控制台输出效果如下

这里写图片描述

这种方式显得代码有点多,还有另外一种方式,代码如下

'use strict';

var React = require('react-native');
var {
  AppRegistry,
  StyleSheet,
  View,
  DeviceEventEmitter,
} = React;

//只需要引入Log
var Log=require('./log');
var AwesomeProject = React.createClass({

  render: function() {
    return (
    <View style={styles.container}>
   </View>
    );
  },
  componentDidMount:function(){
    Log.d("tag","tag");//调用java层log方法,之后就会回调对应的事件(前提是注册了事件)
    //直接使用DeviceEventEmitter进行事件注册
    DeviceEventEmitter.addListener('logInConsole',(e)=>{
       console.log(e);
    });

  },
});

var styles = StyleSheet.create({
  container:{
  },
});
AppRegistry.registerComponent('AwesomeProject', () => AwesomeProject);

效果是一样的。都会在控制台输出。

基本上,掌握了上面的内容,对原始模块的使用也差不多了,本篇文章是基于官方文档的最佳实践Native Modules,但是该文档坑太多,还需谨慎参考。

7
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:918890次
    • 积分:10193
    • 等级:
    • 排名:第1654名
    • 原创:164篇
    • 转载:5篇
    • 译文:0篇
    • 评论:700条
    博客专栏
    我的微博
    最新评论