React Native 原生模块开发 (Android)

这里有一个引导 native-modules-setup,该引导适用于 android/iOS,其实就是自动创建原生模块的模板,之后再去实现自己的逻辑就好了。

可以查看 native-modules-android翻译 】、native-modules-ios翻译】 来了解 android 、IOS 下模块开发的一些 API 和 开发流程。

官网给了一个 Toasts 的简单示例,这里就不用这个了,直接实战化,以集成友盟 SDK 作为例子(备注:以下过程是在 rn 0.61 版本下进行的,版本不同,可能过程也有不同)。

友盟的产品有很多,统计/推送/分享/广告监测等等,分享和广告监测不太是刚需,且会和其他功能交叉(比如分享,集成微信分享的同时一般还会集成支付功能)所有就不集成了,这里仅集成 统计/推送 功能。

目前行业现状:

统计,目前国内用的较多的是 友盟腾讯移动分析,二者在功能上,后者略有胜出,但也仅仅是略有胜出;

推送,最好用的当然是手机厂商的系统级推送通道,目前华为/小米/OPPO/vivo/魅族 都支持,这基本可以覆盖国内 70% (市场份额) 以上的机器了,第三方推送主要是处理剩余那 30% 的,据我个人了解,国内口碑相对可以的是 极光 和 友盟,况且友盟从 CNZZ 起家就开始做统计了,技术积累还是可以的。最后再考虑到注册一个平台就能用两个功能了,不用在不同平台切换,所以推送功能也选友盟。

一、创建项目

// 安装 create-react-native-module 
yarn global add create-react-native-module

// 创建 rn 项目
react-native init sample

// 进入 rn 目录创建 module
cd sample
create-react-native-module uapp

打开 sample 文件夹可以看到已创建了一个 react-native-uapp 文件夹,打开这个文件夹看看目录结构

- android
- index.js
- ios
- package.json
- react-native-uapp.podspec
- README.md

很贴心,连 readme 都帮忙创建好了,不过我看了一下,介绍主要还是针对 rn6.0 以下版本的,后面还是要自己写点使用方法的,暂且不管了,下面的主要工作就是在 android 和 ios 两个文件夹进行了,最后搞一下 index.js 对外提供接口。

二、Android

1、修改命名空间

先来做 android 版本的,进入 android 文件夹,还有一个 readme, 打开看一下,是介绍如何打包为 maven 依赖的,这个就没必要了,可以删了。按照惯例和自己准备用名称,先改一下 android 的命名空间。

修改
src/main/java/com/reactlibrary/UappModule.java
src/main/java/com/reactlibrary/UappPackage.java
头部从 package com.reactlibrary; 改为 package com.umreact.uapp;

移动文件夹:
src/main/java/com/reactlib/ -> src/main/java/com/umreact/uapp

最后修改
build.gradlesrc/main/AndroidManifest.xml,查找 com.reactlibrary 改为 com.umreact.uapp

2、安装本地包

使用 rn 一般都是用 yarn 来管理包的,所以我这里用 yarn 来安装本地包,如果你使用 npm 自行搜索一下,应该都是差不多的,在 sample 目录执行

yarn add file:./react-native-uapp

这样就把这个本地包装上了,对于 android 而言,由于 rn 0.60 之后,是 autolink 的,所以无需其他操作了,可以先 react-native run-android 试一下,看能不能编译成功。

【注意】: 此时真正使用的 react-native-uapp 目录是在 node_modules 下,yarn add 命令自动复制了,所以后续的原生开发应该是在 node_modules/react-native-uapp 目录下。但实际使用的 js 模块却还在 file:./react-native-uapp 目录下

3、设置友盟 sdk

第一步是修改 android/build.gradle, 先来看看这个文件的结构

// 内置函数, 用于安全获取当前使用的 android sdk 版本
// 尝试使用主工程版本, 若无, 则指定一个默认版本
// 在 `android` 块的配置中有使用
def safeExtGet(prop, fallback) {
   ..
}

// 把当前包作为独立工程的编译配置
// 就是直接在当前插件目录执行 `yarn install`,然后编译当前目录
buildscript {
   ...
}

apply plugin: 'com.android.library'
apply plugin: 'maven'

// 当前插件的编译配置
android {
    ....
}

// 也是作为独立工程的配置
// 关于 buildscript 和 repositories 和参考
// https://www.jianshu.com/p/ee57e4de78a3
repositories {
   ....
}

// 当前插件的依赖
dependencies {
  ...
}

// 在 afterEvaluate 中用到了
// 解析 package.json 中的包信息(名称,作者,协议等)
def configureReactNativePom(def pom) {
  ....
}

// Gradle 构建生命周期的钩子
// 对 android 不是特别熟悉,看这部分代码像是打包,可以发布到线上 lib 库的
afterEvaluate { project ->
   ....
}

了解这个文件结构后,来改一下,就拿来做插件的,不需要做独立工程,可以去除相关代码;也不需要发布为lib,所以也可以去除相关代码;当然,这个修改不是必须的,保留这些代码也没什么副作用。

根据友盟的文档 基础组件集成U-Push 集成U-App 集成 可以看出,集成 sdk 有自动和手动两种模式,自动使用 https://dl.bintray.com/umsdk/release 作为 maven 中心库,手动则是在 SDK下载 页面下载 sdk,我这里选择自动化集成,后期维护升级也必将方便;找到需要集成的 sdk,有:

com.umeng.umsdk:utdid:version
com.umeng.umsdk:common:version
com.umeng.umsdk:analytics:version
com.umeng.umsdk:push:version

可以在 https://dl.bintray.com/umsdk/release/ 查看当下的最新版本,最终修改 build.gradle 的代码为

def safeExtGet(prop, fallback) {
    rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}

def safeCfgGet(prop, fallback) {
    return project.hasProperty(prop) ? project.getProperties().get(prop) : fallback
}

apply plugin: 'com.android.library'

android {
    compileSdkVersion safeExtGet('compileSdkVersion', 28)
    buildToolsVersion safeExtGet('buildToolsVersion', "28.0.3")
    defaultConfig {
        minSdkVersion safeExtGet('minSdkVersion', 16)
        targetSdkVersion safeExtGet('targetSdkVersion', 28)
        versionCode 1
        versionName "1.0"
        
        # 创建一个文件设置 与模块相关的混淆规则
        consumerProguardFiles "proguard.pro"

        // +新增,下面会进行说明
        buildConfigField("int", "UMENG_DEVICE_TYPE", safeCfgGet("UMENG_DEVICE_TYPE", "1"))
        buildConfigField("String", "UMENG_PUSH_SECRET", safeCfgGet("UMENG_PUSH_SECRET", "\"\""))
    }
    lintOptions {
        abortOnError false
    }
}

# 添加友盟的线上仓库地址
rootProject.allprojects {
    repositories {
        maven { url 'https://dl.bintray.com/umsdk/release' }
    }
}

dependencies {
    implementation 'com.facebook.react:react-native:+'

    implementation 'com.umeng.umsdk:utdid:1.1.5.3'
    implementation 'com.umeng.umsdk:common:2.1.0'
    implementation 'com.umeng.umsdk:analytics:8.1.3'
    implementation 'com.umeng.umsdk:push:6.0.1'
}

4、开发

src/main/java/com/reactlibrary/UappPackage.java 无需改动
主要是修改 src/main/java/com/reactlibrary/UappModule.java

通过 统计集成推送集成RN 生命周期事件 可以了解到

  1. 载入的 sdk 需要在 application.onCreate 阶段初始化

  2. 统计模块需要在 Activity 的 onResume 和 onPause 触发统计 按照文档其实在 auto 模式下是无需这一步的,但实测了一下,发现 auto 模式有问题)

3.推送模块需要在Activity 的 onCreate 调用 PushAgent.getInstance(context).onAppStart();

  1. RN 可以通过 addLifecycleEventListener 在 Module 中添加 active 生命周期的监听函数
// context - application 上下文
// appkey - 友盟申请得到的 app key
// channel - 渠道,方便后期统计查看(比如小米商店、华为商店),可以为不同市场打包不同apk
// deviceType - 设备类型,支持手机或平板
// pushSecret - 推送秘钥
UMConfigure.init(Context context, String appkey, String channel, int deviceType, String pushSecret);

// 少了 appkey/channel,使用这个调用的前提是
// 在AndroidManifest.xml中配置过appkey和channel值
UMConfigure.init(Context context, int deviceType, String pushSecret);

从初始化函数调用可以看到, appkey、channel、deviceType、pushSecret 作为配置信息,肯定不能在模块中定义的,需要在主工程中设置,主工程要如何传递参数呢,找到了几篇有帮助的文章:gradle-tipsbuildConfigFieldgradle.properties

从这三篇文章可以看出,传递参数可以通过 android 项目根目录的 build.gradlegradle.properties,这两个都是顶级配置文件,任何子工程(插件)都可以读取配置值,很明显的, gradle.properties 作为 kv 配置文件,特别适合做这个。

看完这三篇文章,应该很容易理解上面 build.gradlebuildConfigField() 的作用了,就是为了可以在 gradle.properties 配置友盟参数,但没有设置 appkeychannel,因为这两个可以直接在 AndroidManifest.xml 中设置,并且这样更容易进行分渠道打包,如果有兴趣,可以看看 build-variants

结合上面的信息,最终修改 UappModule.java

package com.umreact.uapp;

import android.content.Context;

import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;

import android.util.Log;
import com.umeng.message.PushAgent;
import com.umeng.commonsdk.UMConfigure;
import com.umeng.analytics.MobclickAgent;
import com.umeng.message.IUmengRegisterCallback;

public class UappModule extends ReactContextBaseJavaModule implements LifecycleEventListener {

    public static void init(Context appContext) {
        // 开启 debug
        UMConfigure.setLogEnabled(true);

        // 初始化友盟
        UMConfigure.init(appContext, BuildConfig.UMENG_DEVICE_TYPE, BuildConfig.UMENG_PUSH_SECRET);

        // 设置 session 时长 (这里为了调试设置的比较小, 可以不设置,保持默认)
        MobclickAgent.setSessionContinueMillis(1000);

        // 设置统计模式 (不使用 auto 模式, 手工触发)
        MobclickAgent.setPageCollectionMode(MobclickAgent.PageMode.MANUAL);

        // 打印 测试统计设备 id
        String[] devices = UMConfigure.getTestDeviceInfo(appContext);
        Log.i("UMLog", "did:" + devices[0]);
        Log.i("UMLog", "mac:" + devices[1]);
        Log.i(
                "UMLog",
                "info:" + "{\"device_id\":\""+devices[0]+"\",\"mac\":\""+devices[1]+"\"}"
        );

        // 注册推送
        final PushAgent mPushAgent = PushAgent.getInstance(appContext);
        mPushAgent.register(new IUmengRegisterCallback() {
            @Override
            public void onSuccess(String deviceToken) {
                Log.i("UMLog","注册成功:deviceToken:-------->  " + deviceToken);
            }
            @Override
            public void onFailure(String s, String s1) {
                Log.e("UMLog","注册失败:-------->  " + "s:" + s + ",s1:" + s1);
            }
        });
    }

    /**
     * 不做特殊开发, 一般 rn 就一个 MainActivity, 但这里也留一个接口
     * 如果有多个 active 的话, 在 active 的 onCreate 调用
     * https://developer.umeng.com/docs/66632/detail/98581
     * @param activity
     */
    public static void onAppStart(Context activity) {
        PushAgent.getInstance(activity).onAppStart();
    }

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

    public UappModule(ReactApplicationContext reactContext) {
        super(reactContext);
        reactContext.addLifecycleEventListener(this);

        // 这里调用, 设备总是处于离线状态, 无法接收推送消息
        // https://developer.umeng.com/docs/66632/detail/98581
        // 官方文档提到: 务必在 Application类的 onCreate() 方法中做SDK代码初始化工作
        // init(reactContext.getApplicationContext());
    }

    @Override
    public void onHostResume() {
        MobclickAgent.onResume(getCurrentActivity());
    }

    @Override
    public void onHostPause() {
        MobclickAgent.onPause(getCurrentActivity());
    }

    @Override
    public void onHostDestroy() {
        // do nothing
    }
}

代码中调用的 BuildConfig.UMENG_DEVICE_TYPEBuildConfig.UMENG_PUSH_SECRET 是在 gradle.properties 文件中配置的

UMENG_DEVICE_TYPE=1
UMENG_PUSH_SECRET="666"

appkey、channel 则在主工程的 AndroidManifest.xml 中配置

<manifest ..>
   ....
   // 添加友盟所需基本权限
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>

    <application ...>
    ....
        <meta-data android:value="666" android:name="UMENG_APPKEY"/>
        <meta-data android:value="Test" android:name="UMENG_CHANNEL"/>
     ...
    </application>
...
</manifest>

还需要在 android/src/main/.../MainApplication.java 添加一行

...
import com.umreact.uapp.UappModule;

public class MainApplication extends Application implements ReactApplication {
  ...
  @Override
  public void onCreate() {
    ..
    // 添加这一行
    UappModule.init(this);
    ..
  }
  ...
}

配置完成后,运行 app;

1、 从 android 的 log 中获取到 {"device_id":"xxx","mac":"xxx"}友盟测试设备 去添加一下,然后在 实时日志 查看是否成功统计,没统计的话,可以尝试重新运行一次 app。

2、从 Log 中找到 deviceToken,到 友盟推送工具 查询设备状态,如果是在线状态,可以到 测试模式 这里添加测试设备,并发送一条推送试一下。

获取 android log 有很多办法,我们需要 filter 的关键词为 UMLog

1、命令行: adb logcat *:S UMLog:V
2、打开 Android Studio 的 Logcat 窗口
3、使用 FB 的 flipper (这个可能需要 rn 版本大于0.60)

5、后记

最终的扩展使用方法为

1、在 gradle.properties 设置 UMENG_DEVICE_TYPEUMENG_PUSH_SECRET
2、在 android/src/main/AndroidManifest.xml 设置 UMENG_APPKEYUMENG_CHANNEL
3、本想除友盟信息外零配置,但奈何 android 开发没有经验,还需在 android/src/main/.../MainApplication.java 调用初始化方法

另外关于 MainApplication.java 中的配置,可以看在 UappModule.javaUappModule() 函数的注释,一开始是想在这里直接载入的,这样就少了一个配置,但实测发现,这样做的话,可以注册成功,但设备总处于离线状态,无法接收推送消息;并且在 推送接入文档 中也有说明:务必在工程的自定义Application类的 onCreate() 方法中做SDK代码初始化工作。所以最终也不再深究了,就先这么着吧(其实排查设备离线,定位到是这里的问题,就花了我好几个钟头,实在懒得继续搞了)

上面的流程只是跑通了统计/推送功能,做一个能用的扩展其实还有不少活要干,比如页面统计、埋点统计、厂商推送通道集成、接收消息后的处理、通过 js 暴露接口给 rn jsx 使用,本篇主要是为了记录一下原生模块开发的流程,所有就不过多着墨了

三、iOS

待填坑....

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,247评论 6 543
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,520评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,362评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,805评论 1 317
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,541评论 6 412
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,896评论 1 328
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,887评论 3 447
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,062评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,608评论 1 336
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,356评论 3 358
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,555评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,077评论 5 364
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,769评论 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,175评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,489评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,289评论 3 400
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,516评论 2 379

推荐阅读更多精彩内容