Android O 行为变更适配方案

Nothing makes an android developer more crazy than a new version of Android.

Android O 在 2017 年 8 月已经正式发布了,Android P 的预览版也已推出,今年下半年也将正式发布。

由于公司的产品面向海外用户较多,大多都已经是 Android 最新版本,且 Google 明确提出:从 2018 年 8 月起,所有向 Google Play 提交的新应用都必须使用 API level 26 (Android 8.0) 及以上版本开发;2018 年 11 月起,所有 Google Play 的现有应用更新同样必须使用 API level 26 及以上版本。因此必须对 Android O 作全面的适配。

1. Android O 新特性

官方文档已经详细描述了 Android O 的新特性和 API,对开发者而言适配 Android O 需要关注以下行为变更:

  • 后台服务限制
  • 隐式广播限制
  • 后台位置限制
  • 通知渠道
  • 权限

2. 后台服务限制

2.1 什么是前台应用

满足以下任意条件的应用被视为处于前台:

  • 具有可见的 Activity
  • 具有前台服务
  • 另一个前台应用关联到该应用,如输入法、壁纸服务、语音服务等

如果以上条件均不满足,应用将被视为处于后台。

2.2 为什么要限制后台服务

后台服务(如网络下载、数据同步等)会消耗手机的内存和电量,影响性能。如果大量应用都开启了后台服务,会严重影响用户体验。

后台服务限制和隐式广播限制从根本上杜绝了后台应用异常消耗系统资源,有助于大幅度降低应用后台行为对设备体验的影响。

2.3 什么是后台服务限制

应用在后台期间保留其后台服务的能力将受到限制。如果应用处于后台时调用了 startService() 将会抛出 IllegalStateException,除非:

  • 应用已经处于前台,则可以调用 startService(),不会抛出 IllegalStateException,但一旦进入后台,后台应用将被置于一个临时白名单中,位于白名单中时,应用可以无限制地启动服务,其后台服务也可以运行。但这个时间窗一过,应用进入空闲状态,后台服务就会被销毁(Nexus 5X 8.0 系统上测试不到1分钟)
  • 启动前台服务
  • 绑定服务,即使应用处于后台也不受影响

如果 targetSdkVersion < 26,是否可以绕过这些限制?

不可以,即使 targetSdkVersion < 26,用户也可以在 Android O 的设备上选择开启这些限制。

2.4 解决方案

  • Job Scheduler
  • Foreground Service
  • Firebase Cloud Messaging and Temporary Service Whitelist

2.5 Job Scheduler

JobScheduler is smarter about when jobs should be run and can batch them together so that devices stay asleep as much as possible.

Google 在 Android 5.0 中引入 JobScheduler 来执行一些需要满足特定条件但不紧急的后台任务,利用 JobScheduler 来执行这些特殊的后台任务来减少电量的消耗。可以将其理解成定时任务,以替代 IntentService + AlarmManager

开发者可以设定需要执行的任务 JobService,以及任务执行的条件 JobInfoJobScheduler 会将任务加入到队列。在特定的条件满足时 Android 系统会去批量的执行所有应用的这些任务,而非对每个应用的每个任务单独处理。这样可以减少设备被唤醒的次数。

2.5.1 JobService

先来看一下 JobService, 它继承自 Service,除了 Service 的一些生命周期方法,又增加了 onStartJobonStopJob 来处理自定义任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyJobService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
Log.i(TAG, "onStartJob");
doJob(params);
return true;
}
@Override
public boolean onStopJob(JobParameters params) {
Log.i(TAG, "onStopJob");
// whether or not you would like JobScheduler to automatically retry your failed job.
return false;
}
private void doJob(JobParameters params) {
// I am on the main thread, so if you need to do background work,
// be sure to start up an AsyncTask, Thread, or IntentService!
}
}

使用 JobService 需要在清单里申请 android.permission.BIND_JOB_SERVICE 权限

1
2
<service android:name=".MyJobService"
android:permission="android.permission.BIND_JOB_SERVICE" />

需要注意的是,在 com.android.server.job.JobServiceContext 类中声明了 EXECUTING_TIMESLICE_MILLIS

1
2
/** Amount of time a job is allowed to execute for before being considered timed-out. */
private static final long EXECUTING_TIMESLICE_MILLIS = 10 * 60 * 1000; // 10mins.

经过测试,不管应用是否处于前台,JobService 都不能无限期运行,有 10 分钟的超时时间,会自动销毁,在 Android L 上这个时间是 1 分钟。因此 Job Scheduler 适用于短耗时的后台任务,不适用于连续的长时间的后台服务。

2.5.2 使用 Job Scheduler

实施一个 Job 包含以下步骤:

  1. JobInfo:采用 Builder 模式,设置 Job 执行的条件和时机
  2. JobServiceService 的子类,Job 执行时的具体行为
  3. android.permission.BIND_JOB_SERVICEJobService 的子类需要授予该权限
  4. JobScheduler:将一个 Job 添加到工作队列中,调用 JobScheduler.enqueue() 或者 JobScheduler.schedule。当工作队列运行时,它可以将待定的工作从队列中剥离并处理(替代 IntentService)。
1
2
3
4
5
6
7
8
9
10
11
void scheduleJob() {
ComponentName componentName = new ComponentName(this, MyService.class);
JobInfo.Builder builder = new JobInfo.Builder(JOB_ID, componentName)
.setMinimumLatency(2000)
.setOverrideDeadline(5000)
// ...
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NONE);
JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
jobScheduler.schedule(builder.build());
}

具体使用可以参考 Scheduling jobs like a pro with JobScheduler

2.5.3 在 API 低于 21 使用 Job Scheduler

JobScheduler 是Google 在 API 21 引入 的,那么对于 API 小于 21 该如何处理。

  • minSdkVersion >= 21:JobScheduler
  • minSdkVersion < 21:Firebase JobDispatcher,API 和 JobScheduler 基本相似,但需要引入 Google Play Services,会增加 apk 的大小。
  • 如果 minSdkVersion < 21,可以做一层封装,业务方只调一套,也可以只用Firebase JobDispatcher 兼容,看业务方需求。

2.5.4 JobIntentService

JobIntentService 继承自 Service,可以用来简化处理任务。调用 JobIntentService.enqueueWork(),即可执行任务。它会根据 API 版本进一步执行

  • 在 Android O 及之后的设备上,调用 JobScheduler.enqueue()
  • 在 Android O 之前的设备上,调用 Context.startService()

JobIntentService 需要 android.permission.WAKE_LOCK 的权限

1
<uses-permission android:name=”android.permission.WAKE_LOCK” />

2.6 Foreground Service

Job Scheduler 只适用于短耗时的后台任务,如果需要在后台执行长期的任务,推荐使用 Foreground Service。这样应用会在通知栏展示进行中的通知,以告知用户你的应用正在运行后台任务。

在 Android O 之前,创建前台服务的方式通常是先创建一个后台服务,然后将该服务推到前台。

但对于 Android O,系统不允许后台应用创建后台服务。 因此,Android O 引入了一种全新的方法,即
ContextCompat.startForegroundService(),以在前台启动新服务。

  1. 调用 ContextCompat.startForegroundService() 可以创建一个前台服务,相当于创建一个后台服务并将它推到前台。

  2. 创建一个用户可见的 Notification

  3. 必须立即(在5秒内)调用该服务的 startForeground(id: Int, notification: Notification) 方法,否则将停止服务并抛出 android.app.RemoteServiceException: Context.startForegroundService() did not then call Service.startForeground() 异常。

2.7 Firebase Cloud Messaging and Temporary Service Whitelist

使用 FCM 需要引入不低于 10.2.1Google Play Services SDK

当应用出现以下情况时,该应用可以被暂时加入到一个白名单里,应用可以像跑前台服务一样跑后台服务:

  • 处理高优先级的 FCM 消息
  • 接收广播,如短信/彩信消息
  • 点击通知执行 PendingIntent

这种情况常用的一个场景是,我们的应用需要通过请求后台服务更新数据,可以给我们的应用发送一个高优先级的 FCM 消息,即使系统处于休眠状态,也能马上收到 FCM 消息,被加到白名单后,就可以启动一个后台服务来更新数据了。

通过以上情况启动的也必须是短耗时的任务,如 JobScheduler 或者 JobIntentService

2.8 解决方案

  • 对于短耗时的特定任务,采用 Job Scheduler
  • 对于需要长期执行的服务,采用 Foreground Service
  • 对于一些三方的服务,无法修改,如果没有适配 Android O,可以在启动这些服务前启动一个空的 Foreground Service,这样应用处于前台,就可以启动这些后台服务了。

如果在应用初始化后启动一个空的 Foreground Service,保证应用处于前台,则旧的 Service 都可以正常,修改量最小。但不推荐。

3. 隐式广播限制

3.1 为什么要限制隐式广播

如果在 AndroidManifest.xml 中随意地声明隐式广播,那么任何时候收到响应系统事件都会唤醒应用,即使应用当前处于休眠状态,导致异常消耗系统资源。

3.2 什么是隐式广播限制

应用无法在 AndroidManifest.xml 中声明隐式广播接收器,以获得绝大部分响应系统事件的后台能力。

显式广播依然能在 AndroidManifest.xml 中注册。

3.3 解决方案

  • Broadcast Whitelist
  • Scheduling Jobs
  • Dynamic Broadcasts

3.3.1 白名单

以下事件依然能在 AndroidManifest.xml 中声明隐式广播接收器,可以正常使用。

  • ACTION_LOCKED_BOOT_COMPLETED, ACTION_BOOT_COMPLETED
  • ACTION_USER_INITIALIZE
  • ACTION_LOCALE_CHANGED
  • ACTION_USB_ACCESSORY_ATTACHED, ACTION_USB_ACCESSORY_DETACHED, ACTION_USB_DEVICE_ATTACHED, ACTION_USB_DEVICE_DETACHED
  • ACTION_HEADSET_PLUG
  • ACTION_CONNECTION_STATE_CHANGED, ACTION_CONNECTION_STATE_CHANGED, ACTION_ACL_CONNECTED, ACTION_ACL_DISCONNECTED
  • ACTION_CARRIER_CONFIG_CHANGED
  • LOGIN_ACCOUNTS_CHANGED_ACTION
  • ACTION_PACKAGE_DATA_CLEARED
  • ACTION_PACKAGE_FULLY_REMOVED
  • ACTION_NEW_OUTGOING_CALL
  • ACTION_DEVICE_OWNER_CHANGED
  • ACTION_EVENT_REMINDER
  • ACTION_MEDIA_MOUNTED, ACTION_MEDIA_CHECKING, ACTION_MEDIA_UNMOUNTED, ACTION_MEDIA_EJECT, ACTION_MEDIA_UNMOUNTABLE
  • SMS_RECEIVED_ACTION, WAP_PUSH_RECEIVED_ACTION

3.3.2 JobScheduler

JobScheduler 可以用来执行一些需要满足特定条件的后台任务,如设备网络状态变化、设备充电状态变化、低电量等。因此大多数情况下,之前注册隐式广播的应用使用 JobScheduler 可以获得类似的功能。

3.3.3 动态注册广播

依然能用 Context.registerReceiver() 动态的注册隐式广播,不受影响。但务必在不需要的时候(如生命周期结束)调用 Context.unregisterReciever()

4. 后台位置限制

在 Android O 上,应用处于后台时降低了后台应用接收位置更新的频率,具体的位置行为和受影响的 API 可以查看官方文档

5. 通知渠道

所有通知的实现都需要提供通知渠道(Notification ChannelId),否则通知在 Android O 系统上无法正常展示,会弹 Toast 提示 Developer warning for package XXX,Failed to post notification on channel “null”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.O) {
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannelGroup group = new NotificationChannelGroup(GROUP_ID, GROUP_NAME);
manager.createNotificationChannelGroup(group);
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT);
channel.setGroup(GROUP_ID);
// ...
manager.createNotificationChannel(channel);
notification = new Notification.Builder(getApplicationContext(), CHANNEL_ID)
// ...
.build();
} else {
notification = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
// ...
.build();
}

6. 权限

  • Android O 之前,申请一个子权限(如写外部存储权限),会自动获取权限组中其他子权限(读外部存储权限)。组内其他子权限可以直接使用,无需申请。

  • Android O 修复了这个错误。在 Android O 上,申请一个子权限,组内其他子权限不会自动获取,需要再次申请才能使用。但不会弹出系统的权限申请框,将被自动批准。

7. Reference

本文是 慌不要慌 原创,发表于 https://danke77.github.io/,请阅读原文支持原创 https://danke77.github.io/2018/06/09/target-android-o/,版权归作者所有,转载请注明出处。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!



React Native 原生模块和 JS 模块交互(Android)

1. JS 模块调用原生模块方法

1.1 ReactContextBaseJavaModule

创建一个原生模块 ZanIntentModule 并继承抽象类 ReactContextBaseJavaModule,同时实现几个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class ZanIntentModule extends ReactContextBaseJavaModule {
public ZanIntentModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "ZanIntentModule";
}
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
return constants;
}
/**
* js call native to start activity
*
* @param toActivityName
* @param map
*/
@ReactMethod
public void startActivity(String toActivityName, ReadableMap map) {
try {
Activity currentActivity = getCurrentActivity();
if (currentActivity != null) {
Class toActivity = Class.forName(toActivityName);
Intent intent = getActivityIntent(currentActivity, toActivity, map);
currentActivity.startActivity(intent);
}
} catch (Exception e) {
throw new JSApplicationIllegalArgumentException(
"Could not open Activity : " + e.getMessage());
}
}
}

getName()

1
2
3
4
@Override
public String getName() {
return "ZanIntentModule";
}

这个函数用于返回一个字符串名字,这个名字在 JavaScript 端标记这个模块。这里我们把这个模块命名为 ZanIntentModule,这样就可以在 JavaScript 中通过 NativeModules.ZanIntentModule 访问到这个模块。

getConstants()

1
2
3
4
5
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
return constants;
}

如果需要在 Java 和 JavaScript 之间定义常量,则要覆盖这个方法,返回需要导出给 JavaScript 使用的常量。

@ReactMethod

要导出一个方法给 JavaScript 使用,Java 方法需要使用注解 @ReactMethod,方法的返回类型必须为 void。React Native 的跨语言访问是异步进行的,所以想要给 JavaScript 返回一个值的唯一办法是使用回调函数或者发送事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ReactMethod
public void startActivity(String toActivityName, ReadableMap map) {
try {
Activity currentActivity = getCurrentActivity();
if (currentActivity != null) {
Class toActivity = Class.forName(toActivityName);
Intent intent = getActivityIntent(currentActivity, toActivity, map);
currentActivity.startActivity(intent);
}
} catch (Exception e) {
throw new JSApplicationIllegalArgumentException(
"Could not open Activity : " + e.getMessage());
}
}

在 JavaScript 中可以这样调用

1
2
3
4
5
NativeModules.ZanIntentModule.startActivity(
'com.danke77.sample.activity.WebViewActivity',
{
'url': url
})

1.2 注册模块

创建一个 Package 类 ZanReactPackage 并实现 ReactPackage,在 createNativeModules 方法中添加这个模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ZanReactPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new ZanIntentModule(reactContext));
return modules;
}
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}

1.3 添加模块

Application 中的 ReactNativeHost 实例里 getPackages 方法添加 ZanReactPackage 实例

1
2
3
4
5
6
7
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new ZanReactPackage()
);
}

1.4 回调函数

Callback

Callback 是 com.facebook.react.bridge 中的一个接口,作为 ReactMethod 的一个传参,用来映射 JavaScript 的回调函数(function)。

Callback 接口只定义了一个方法 invoke,invoke 接受多个参数,这个参数必须是 com.facebook.react.bridge 中支持的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@ReactMethod
public void login(String name, String password, Callback success, Callback failure) {
try {
if (TextUtils.isEmpty(name)) {
failure.invoke("name is empty");
return;
}
if (TextUtils.isEmpty(password)) {
failure.invoke("password is empty");
return;
}
if (new LoginTask().login(name, password)) {
success.invoke(name);
} else {
failure.invoke("login failure");
}
} catch (Exception e) {
e.printStackTrace();
failure.invoke(e.getMessage());
}
}

在 JavaScript 中可以这样调用

1
2
3
4
5
NativeModules.LoginModule.login('name', 'password', (name) => {
alert(name)
}, (err) => {
alert(err)
})

Promise

Promise 是 ES6 中增加的对于异步编程和回调更加友好的 API。

com.facebook.react.bridge 中定义的 Promise 接口,实现了 resolvereject 方法,resolve 用来处理正确结果,reject 用来处理异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@ReactMethod
public void login(String name, String password, Promise promise) {
try {
if (TextUtils.isEmpty(name)) {
promise.reject("-1", "name is empty");
return;
}
if (TextUtils.isEmpty(password)) {
promise.reject("-2", "password is empty");
return;
}
if (new LoginTask().login(name, password)) {
WritableMap map = Arguments.createMap();
map.putString("name", name);
promise.resolve(map);
} else {
promise.reject("-3", "login failure");
}
} catch (Exception e) {
e.printStackTrace();
promise.reject(e);
}
}

在 JavaScript 中可以这样调用

1
2
3
4
5
6
7
8
9
10
11
NativeModules.LoginModule.login('name', 'password')
.then(
(map) => {
alert(map.name)
}
)
.catch(
(code, err) => {
alert(err)
}
)

或者用 async/await 来修饰,以同步方式调用原生模块

1
2
3
4
5
6
7
8
9
10
11
async login() {
try {
var {
name
} = await NativeModules.LoginModule.login('name', 'password')
alert(name)
}
catch (code, err) {
alert(err)
}
}

在原生模块中 Promise 类型的参数必须要放在最后一位,这样 JavaScript 调用的时候才能返回一个 Promise。

2. 原生模块发送事件到 JS 模块

原生模块可以在没有被调用的情况下往 JavaScript 发送事件通知。

1
2
3
4
5
6
7
protected void sendEvent(ReactContext reactContext,
String eventName,
@Nullable WritableMap params) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}

发送事件时

1
2
3
WritableMap params = Arguments.createMap();
params.putInt("unread", unreadCount);
sendEvent(getReactApplicationContext(), "onRefreshMessage", params);

向 JavaScript 模块发送了一个名为 onRefreshMessage 的事件,并携带了 params 作为参数。

JavaScript 模块可以通过使用 DeviceEventEmitter 模块来监听事件

1
2
3
4
5
6
7
8
9
10
11
componentDidMount() {
DeviceEventEmitter.addListener('onRefreshMessage', this.onUpdateMessage)
}
componentWillUnmount() {
DeviceEventEmitter.removeListener('onRefreshMessage', this.onUpdateMessage)
}
onUpdateMessage = (e) => {
alert(e.unread)
}

本文是 慌不要慌 原创,发表于 https://danke77.github.io/,请阅读原文支持原创 https://danke77.github.io/2016/12/07/react-native-native-modules-android/,版权归作者所有,转载请注明出处。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!



在 Fragment 中使用 React Native

React Native 官网提供了在 Activity 中使用 React Native 的方法,最近项目中需要在 Fragment 中使用 React Native,参考 http://stackoverflow.com/questions/35221447/react-native-inside-a-fragment 及各种尝试摸索后总结方法如下。

1. MyApplication

MyApplication 除了实现 ReactApplication 的抽象方法 getReactNativeHost 外,还需要获取到 ReactContext 并提供 get 接口,因为在 Fragment 里无法获取到 ReactContext,只能获取 Context,而原生调用 js 时使用 sendEvent 又需要用到 ReactContext

Fragment 中通过 ReactInstanceManager#getCurrentReactContext 获取到的 ReactContext 为空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class MyApplication implements ReactApplication {
// ...
private ReactContext mReactContext;
public ReactContext getReactContext() {
return mReactContext;
}
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
protected boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new MyReactPackage(),
new OtherReactPackage()
// ...
);
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
private void registerReactInstanceEventListener() {
mReactNativeHost.getReactInstanceManager().addReactInstanceEventListener(mReactInstanceEventListener);
}
private void unRegisterReactInstanceEventListener() {
mReactNativeHost.getReactInstanceManager().removeReactInstanceEventListener(mReactInstanceEventListener);
}
private final ReactInstanceManager.ReactInstanceEventListener mReactInstanceEventListener = new ReactInstanceManager.ReactInstanceEventListener() {
@Override
public void onReactContextInitialized(ReactContext context) {
mReactContext = context;
}
};
@Override
public void onCreate() {
// ...
registerReactInstanceEventListener();
}
}

ApplicationonCreate 方法里注册一个 ReactInstanceEventListener,用于初始化后获取到 ReactContext

2. ReactInstanceManager

通过 ReactNativeHost#getReactInstanceManager 可以获取 ReactInstanceManager 这个抽象类,它提供了 ReactInstanceEventListener 接口及相应的添加和删除方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Add a listener to be notified of react instance events.
*/
public abstract void addReactInstanceEventListener(ReactInstanceEventListener listener);
/**
* Remove a listener previously added with {@link #addReactInstanceEventListener}.
*/
public abstract void removeReactInstanceEventListener(ReactInstanceEventListener listener);
/**
* Listener interface for react instance events.
*/
public interface ReactInstanceEventListener {
/**
* Called when the react context is initialized (all modules registered). Always called on the
* UI thread.
*/
void onReactContextInitialized(ReactContext context);
}

3. BaseReactFragment

BaseReactFragment 继承自自己封装的 Fragment 基类 BaseFragment,这里需要用到 ReactRootViewReactInstanceManager

它们在 FragmentonAttach 方法中获取,并在 onCreateView 方法中返回该 ReactRootView

onActivityCreated 方法中即可使用我们的 React Native 组件,这里需要子类实现 getMainPageName 抽象方法,获取到对应的 React Native 组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public abstract class BaseReactFragment extends BaseFragment {
private ReactRootView mReactRootView;
private ReactInstanceManager mReactInstanceManager;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mReactRootView = new ReactRootView(activity);
mReactInstanceManager = ((MyApplication) getActivity().getApplication()).getReactNativeHost().getReactInstanceManager();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
return mReactRootView;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mReactRootView.startReactApplication(mReactInstanceManager, getMainPageName(), null);
}
protected abstract String getMainPageName();
protected void sendEvent(String eventName,
@Nullable WritableMap params) {
if (((MyApplication) getActivity().getApplication()).getReactContext() != null) {
((MyApplication) getActivity().getApplication()).getReactContext()
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}
}
}

sendEvent 方法用于原生调用 js 的接口,需要获取到 ReactContext 对象,通过 ReactInstanceManager#getCurrentReactContext 获取到的 ReactContext 为空,这里从 Application 中获取。

创建一个 BaseReactFragment 的子类用于装载 React Native 组件

1
2
3
4
5
6
public class MyFragment extends BaseReactFragment {
@Override
public String getMainPageName() {
return "MyComponent"; // name of our React Native component we've registered
}
}

4. BaseReactActivity

BaseReactFragment 所在的 Activity 必须实现 DefaultHardwareBackBtnHandler,用于绑定 React Native 组件的生命周期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class BaseReactActivity extends BaseActivity implements DefaultHardwareBackBtnHandler {
/*
* Get the ReactInstanceManager, AKA the bridge between JS and Android
* We use a singleton here so we can reuse the instance throughout our app
* instead of constantly re-instantiating and re-downloading the bundle
*/
private ReactInstanceManager mReactInstanceManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/**
* Get the reference to the ReactInstanceManager
*/
mReactInstanceManager =
((MyApplication) getActivity().getApplication()).getReactNativeHost().getReactInstanceManager();
}
@Override
public void invokeDefaultOnBackPressed() {
super.onBackPressed();
}
/*
* Any activity that uses the ReactFragment or ReactActivty
* Needs to call onHostPause() on the ReactInstanceManager
*/
@Override
protected void onPause() {
super.onPause();
if (mReactInstanceManager != null) {
mReactInstanceManager.onHostPause();
}
}
/*
* Same as onPause - need to call onHostResume
* on our ReactInstanceManager
*/
@Override
protected void onResume() {
super.onResume();
if (mReactInstanceManager != null) {
mReactInstanceManager.onHostResume(this, this);
}
}
}

本文是 慌不要慌 原创,发表于 https://danke77.github.io/,请阅读原文支持原创 https://danke77.github.io/2016/11/23/react-native-inside-fragment/,版权归作者所有,转载请注明出处。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!



理解 Flux

1. Flux 是什么

An application architecture for React utilizing a unidirectional data flow.

flux-architecture

Flux 是利用单向数据流的形式来组合 React 组件的应用架构思想。它不是一个实现好的框架,目前有很多基于 Flux 的三方实现,本文采用的是 Facebook 的官方实现

2. Flux 数据流

flux-data-flow

Action

Action 是用户与 View 层交互后发出的消息,如点击事件等。多个 Action 可以由 actionType 来区分。

Dispatcher

Dispatcher 是整个应用的事件分发中心,管理所有的数据流。接收 Action 传来的数据后,执行 Store 注册的回调函数,将 Action 提供的数据发送给 Store。

对单个应用 Dispatcher 是单例的。可以在执行回调函数前执行 debug 操作、日志操作、权限操作等。

Store

Store 封装了所有的业务逻辑和数据处理。所有的数据变化都发生在 Store 内部。Store 对外只提供了 get 接口,不允许提供 set 接口。View 不允许直接操作 Store。所有的数据都是由 Dispatcher 收到 View 触发的 Action 后,发送到 Store,再触发 change 事件传回到 View。

View & ControllerView

ControllerView 可以理解为容器组件,类似 MVC 中的 controller,包含了一个或多个 View 子组件。

数据由 Store 传递到 ControllerView 后,通过 setState 修改 ControllerView 的状态,再通过属性传递到各个 View 子组件。View 不允许有自己的状态,所有的数据只能通过属性从 ControllerView 获取,所有的表现都由 ControllerView 决定。

因此 View 做的非常薄,只关心交互及触发不同的 Action。

Flux 如何工作

View –> Action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// MyListView.js
export default class MyListView extends Component {
// ...
render() {
return (
<ScrollView style={styles.container}>
<View>
<Text style={styles.add} onPress={this.props.addItemClickFunc}>Click me to Add!</Text>
{
this.props.items.map((item, index) =>
<Text key={index} style={styles.item}>{index}: {item}</Text>
)
}
</View>
</ScrollView>
);
}
}

this.props.addItemClickFunc 由 ControllerView 通过属性传递给 View。点击 View 中的 Click me to Add! 就会触发 Action。

1
2
3
4
// MyListViewController.js
_onAddItemClick() {
ButtonActions.addItem('item')
}

Action –> Dispatcher

1
2
3
4
5
6
7
8
9
// ButtonActions.js
const ButtonActions = {
addItem(item) {
AppDispatcher.dispatch({
actionType: ADD_ITEM,
actionItem: item
})
}
}

触发 Action 后,由事件分发中心 Dispatcher 来统一处理。

Dispatcher –> Store

1
2
3
4
5
6
7
8
9
10
// AppDispatcher.js
AppDispatcher.register((action) => {
switch(action.actionType) {
case ADD_ITEM:
ListStore.addItem(action.actionItem)
ListStore.emitChange()
break
default:
}
})

Dispatcher 根据 actionType 来执行不同的 Store 回调,将 Action 提供的数据发送给 Store。

Store –> ControllerView –> View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ListStore.js
const ListStore = assign({}, EventEmitter.prototype,
{
items: [],
getAll() {
return this.items
},
addItem(item) {
this.items.push(item)
},
emitChange() {
this.emit('change')
},
addChangeListener(callback) {
this.on('change', callback)
},
removeChangeListener(callback) {
this.removeListener('change', callback)
}
}
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// MyListViewController.js
export default class MyListViewController extends Component {
constructor(props) {
super(props)
this.state = {
items: ListStore.getAll()
}
}
componentDidMount() {
ListStore.addChangeListener(this._onListChange.bind(this))
}
componentWillUnmount() {
ListStore.removeChangeListener(this._onListChange.bind(this))
}
_onListChange() {
this.setState({
items: ListStore.getAll()
})
}
_onAddItemClick() {
ButtonActions.addItem('item')
}
render() {
return (
<MyListView
items={this.state.items}
addItemClickFunc={this._onAddItemClick}/>
)
}
}

Store 数据发生变化后执行 emitChange,告诉 ControllerView 通过 setState 改变状态,再执行 render 通过属性下发数据改变 View。

详细例子可以参考 https://github.com/danke77/mastering-flux

本文是 慌不要慌 原创,发表于 https://danke77.github.io/,请阅读原文支持原创 https://danke77.github.io/2016/10/25/understanding-flux/,版权归作者所有,转载请注明出处。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!



原生 iOS 项目集成 React Native

创建一个 React Native 项目并写一个纯的 React Native 应用可以参考官方指南

Android 项目集成 React Native 可以参考 原生 Android 项目集成 React Native

本文主要介绍原生 iOS 项目集成 React Native 并用于部分页面开发的流程。开发环境为 macOS 10.12、Xcode 8.0、React Native 0.35.0。而官方给出的 植入原生 iOS 应用指南 只对应到 0.28 版本。最新版(当前为 0.35)的集成方案稍微有些变动。

0. 安装 CocoaPods

iOS 开发者可以跳过这一步。

0.0 使用 rvm 安装/更新 ruby 环境

安装 cocoapods 对 ruby 版本有要求

1
2
3
4
5
6
7
$ curl -L https://get.rvm.io | bash -s stable
$ source ~/.rvm/scripts/rvm
// 查看远程 ruby 版本
$ rvm list known
// 查看本地 ruby 版本
$ rvm list
$ rvm install 2.3.0

0.1 使用 gem 安装 cocoapods

如要修改 gem 的镜像地址

1
2
3
4
5
6
$ gem sources -l
// 删除已有的源地址
$ gem sources -r https://rubygems.org/
// 添加需要的源地址
$ gem sources -a https://ruby.taobao.org/
$ gem sources -l

安装 cocoapods

1
2
3
$ gem install cocospods
$ cd ~/.cocoapods/repos/
$ git clone https://github.com/CocoaPods/Specs.git master

顺利的话安装 ruby 和 cocoapods 两步都会成功,如果失败了可以针对具体问题去网上搜索相关教程或解决方案。这里不详述。

1. 创建/修改 iOS 项目

集成 React Native 要求 iOS 系统版本不小于 7.0。

2. 添加 package.json

在 iOS 项目根目录新建文件 package.json,内容如下(参考 react-native init 生成的 package.json 文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"name": "react-native-sample",
"version": "0.0.1",
"description": "sample of react native embedding ios",
"main": "index.ios.js",
"private": true,
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start"
},
"author": "danke77",
"license": "ISC",
"dependencies": {
"react": "^15.3.2",
"react-native": "^0.35.0"
},
"devDependencies": {
}
}

执行 npm install 就可以安装 dependencies 下的 npm 组件了。

这个时候在 iOS 项目根目录就生成了 node_modules/ 文件夹,里面就是一些用到的组件。

执行 react-native upgrade 可以更新已有组件。

3. 添加 index.ios.js

在 iOS 项目根目录创建目录 js/,js 相关的代码就放在这个文件夹下。

js/ 下添加 App.js,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, { Component } from 'react'
import { View, Text, StyleSheet } from 'react-native'
export default class extends Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.text}>
Hello React Native!
</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#ffffff'
},
text: {
fontSize: 20,
color: '#333333'
}
})

在 iOS 项目根目录新建文件 index.ios.js,内容如下

1
2
3
4
import { AppRegistry } from 'react-native'
import App from './js/App'
AppRegistry.registerComponent('navigation', () => App)

这里的 navigation 一般会根据模块功能命名,后面还会用到。

当然也可以把 App.js 的内容写在 index.ios.js 里,但这样写更清晰一些,尤其是项目大了文件多的情况。

4. 用 cocoapods 集成 React Native

在 iOS 项目根目录的 Podfile 文件(没有则创建)添加 React Native 相关内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
target 'HelloReactNative' do
platform :ios, '7.0'
source 'https://github.com/CocoaPods/Specs.git'
# 这里的 :path 内容取决于 node_modules/ 实际所在的位置
pod 'React', :path => ‘./node_modules/react-native', :subspecs => [
'Core',
'CSSLayout',
'RCTText',
'RCTImage',
'RCTNetwork',
'RCTWebSocket', # needed for debugging
# Add any other subspecs you want to use in your project
]

要在这里添加项目需要的依赖,如要使用 React Native 的 Text 则必须要添加 RCTText 依赖(pod 'React/RCTText')。

然后在根目录执行 pod install

如果报错说找不到 CSSLayout,则需要在 node_modules/react-native/React.podspec 里添加

1
2
3
4
s.subspec 'CSSLayout' do |ss|
ss.source_files = "React/CSSLayout/**/*.{c,h}"
ss.header_mappings_dir = "React"
end

并重新执行 pod install。如果在 Pods/Development Pods/ 下有 React/,且 React/ 下有 CoreCSSLayoutRCTText 等则说明成功。

.gitignore 中添加

1
2
3
# node.js
# node_modules/
npm-debug.log

5. React Native 相关的 ViewController

创建一个用于容纳 React Native 组件的 ViewController,并添加 RCTRootView,它会把 React Native 组件解析成原生的 UIView。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#import "HelloReactViewController.h"
#import <RCTRootView.h>
@interface HelloReactViewController ()
@end
@implementation HelloReactViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
#ifdef DEBUG
NSURL * jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];
#else
NSURL * jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"bundle/index.ios" withExtension:@"bundle"];
#endif
RCTRootView * rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"navigation"
initialProperties:nil
launchOptions:nil];
self.view = rootView;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end

jsCodeLocation 是 React Native 资源加载的路径,可以通过网络加载本地的资源文件(主要用于本地调试),或者将其打包成 js bundle 文件(用于发布正式包)。

moduleName 对应 React Native 组件的入口,必须和前面的 AppRegistry.registerComponent('navigation', () => App) 里的 navigation 对应。

6. 启动服务

debug 模式下需要启动 package server,在 package.json 所在目录(一般为项目根目录)下执行 npm start,它等效于 package.jsonscripts 下的 node node_modules/react-native/local-cli/cli.js start,相当于启动一个本地服务。

Terminal 显示如下表示服务已正常启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
> react-native-module@0.0.1 start /Users/danke77/Projects/react-native/HelloReactNative
> node node_modules/react-native/local-cli/cli.js start
Scanning 581 folders for symlinks in /Users/danke77/Projects/react-native/HelloReactNative/node_modules (17ms)
┌────────────────────────────────────────────────────────────────────────────┐
│ Running packager on port 8081. │
│ │
│ Keep this packager running while developing on any JS projects. Feel │
│ free to close this tab and run your own packager instance if you │
│ prefer. │
│ │
│ https://github.com/facebook/react-native │
│ │
└────────────────────────────────────────────────────────────────────────────┘
Looking for JS files in
/Users/danke77/Projects/react-native/HelloReactNative
[2016-10-17 17:06:48] <START> Building Dependency Graph
[2016-10-17 17:06:48] <START> Crawling File System
[Hot Module Replacement] Server listening on /hot
React packager ready.
[2016-10-17 17:06:49] <END> Crawling File System (966ms)
[2016-10-17 17:06:49] <START> Building in-memory fs for JavaScript
[2016-10-17 17:06:49] <END> Building in-memory fs for JavaScript (260ms)
[2016-10-17 17:06:49] <START> Building in-memory fs for Assets
[2016-10-17 17:06:50] <END> Building in-memory fs for Assets (138ms)
[2016-10-17 17:06:50] <START> Building Haste Map
[2016-10-17 17:06:50] <START> Building (deprecated) Asset Map
[2016-10-17 17:06:50] <END> Building (deprecated) Asset Map (104ms)
[2016-10-17 17:06:50] <END> Building Haste Map (428ms)
[2016-10-17 17:06:50] <END> Building Dependency Graph (1825ms)

7. 开发调试

模拟器上可以通过工具栏的 Hardware->Shake Gesture 或快捷键调出开发调试菜单;真机上可以通过摇一摇调出。

8. 发布正式包

React Native 的开发版需要有一个 package server 随时发送更新后的 js bundle 文件。如果要打正式包,需要把 js bundle 文件保存到 iOS 项目的目录下。这样,正式包就不需要 server 支持了,可独立运行。

在根目录下创建 bundle/ 文件夹,执行以下命令将 js bundle 保存到资源目录下

1
$ react-native bundle --platform ios --dev false --entry-file index.ios.js --bundle-output ./bundle/index.ios.bundle --assets-dest ./bundle

bundle/ 下就会生成 index.ios.bundle 文件及 assets/ 文件夹,后者会放 React Native 中用到的资源如图片等。

然后将生成的 bundle/ 文件夹以 Create folder references 的形式导入到工程里,就可以打正式包了。

本文是 慌不要慌 原创,发表于 https://danke77.github.io/,请阅读原文支持原创 https://danke77.github.io/2016/10/19/react-native-embedding-ios/,版权归作者所有,转载请注明出处。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!