
目錄
項(xiàng)目源碼:react-native-splash-screen
問(wèn)題描述:
用React Native架構(gòu)的無(wú)論是Android APP還是iOS APP,在啟動(dòng)時(shí)都出現(xiàn)白屏現(xiàn)象,時(shí)間大概1~3s(根據(jù)手機(jī)或模擬器的性能不同而不同)。
問(wèn)題分析:
為什么會(huì)產(chǎn)生白屏?
React Native應(yīng)用在啟動(dòng)時(shí)會(huì)將js bundle讀取到內(nèi)存中,并完成渲染。這期間由于js bundle還沒(méi)有完成裝載并渲染,所以界面顯示的是白屏。
白屏給人的感覺(jué)很不友好,那有沒(méi)有辦法不顯示白屏呢?
上文解釋了:為什么React Native應(yīng)用會(huì)在啟動(dòng)的時(shí)候顯示一會(huì)白屏。既然知道了出現(xiàn)問(wèn)題的原因,那么離解決問(wèn)題也不遠(yuǎn)了。市場(chǎng)上大部分APP在啟動(dòng)的時(shí)候都會(huì)有個(gè)啟動(dòng)屏,啟動(dòng)屏對(duì)于用戶(hù)是比較友好的,一來(lái)展示歡迎信息,二來(lái)顯示一些產(chǎn)品信息或一些廣告,啟動(dòng)頁(yè)對(duì)于程序來(lái)說(shuō),是為程序完成初始化加載數(shù)據(jù),做一些初始化工作的所保留的時(shí)間,啟動(dòng)屏等待的時(shí)間可長(zhǎng)可短,具體根據(jù)業(yè)務(wù)而定。
下面我就教大家如何給React Native 應(yīng)用添加啟動(dòng)屏,并解決啟動(dòng)白屏的問(wèn)題。
Android啟動(dòng)白屏解決方案
我們可以通過(guò)為React Native Android應(yīng)用添加啟動(dòng)屏的方式,來(lái)解決啟動(dòng)白屏的問(wèn)題。我在《React Native Android啟動(dòng)屏,啟動(dòng)白屏,閃現(xiàn)白屏》一文中介紹過(guò)一種為React Native Android應(yīng)用添加啟動(dòng)屏的方法,
不過(guò)那種方法雖好,但牽扯到對(duì)React Native 源碼的修改,如果React Native 版本有更新還需要對(duì)源碼做一些處理,所以以后維護(hù)起來(lái)不是很方便。
下面就向大家介紹另外一種為React Native Android應(yīng)用添加啟動(dòng)屏的方案。
在《React Native Android啟動(dòng)屏,啟動(dòng)白屏,閃現(xiàn)白屏》一文中
我們使用的是在根視圖容器上添加一個(gè)視圖作為啟動(dòng)屏,當(dāng)js bundle加載并渲染完成后,再將添加的視圖從根視圖上移除。在根視圖上添加一個(gè)視圖的方式其實(shí)就是為了遮擋白屏,既然是遮擋白屏,我們是不是可以彈出一個(gè)對(duì)話框呢?
小伙伴們肯定會(huì)說(shuō),對(duì)話框也不是全屏啊,主題也不一樣啊,不過(guò)沒(méi)關(guān)系,既然我們可以添加對(duì)話框,那么我們就可以修改對(duì)話框的樣式來(lái)達(dá)到我們需要的效果。
要達(dá)到啟動(dòng)屏的效果,我們需要一個(gè)什么樣效果的對(duì)話框呢?
- 在APP啟動(dòng)的時(shí)候顯示;
- 在js bundle加載并渲染完成后消失;
- 全屏顯示;
- 顯示的內(nèi)容可以通過(guò) layout xml 進(jìn)行修改;
上述是我們對(duì)這個(gè)對(duì)話框的基本需求,現(xiàn)在就讓我們來(lái)實(shí)現(xiàn)這一需求:
第一步,創(chuàng)建一個(gè)對(duì)話框組件SplashScreen
為滿(mǎn)足上述需求,對(duì)話框組件需要提供下面兩個(gè)方法:
1.顯示對(duì)話框的方法:
/**
* 打開(kāi)啟動(dòng)屏
*/
public static void show(final Activity activity,final boolean fullScreen) {
if (activity == null) return;
mActivity = new WeakReference<Activity>(activity);
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
if (!activity.isFinishing()) {
mSplashDialog = new Dialog(activity,fullScreen? R.style.SplashScreen_Fullscreen:R.style.SplashScreen_SplashTheme);
mSplashDialog.setContentView(R.layout.launch_screen);
mSplashDialog.setCancelable(false);
if (!mSplashDialog.isShowing()) {
mSplashDialog.show();
}
}
}
});
}
為了Activity被銷(xiāo)毀的時(shí)候,持有的Activity能被及時(shí)的回收,這里我們通過(guò)new WeakReference<Activity>(activity);創(chuàng)建了一個(gè)Activity的弱引用。
另外,因?yàn)樵贏ndroid中所有的有關(guān)UI操作都必須在主線程,所有我們通過(guò)activity.runOnUiThread(new Runnable()...,將對(duì)話框的顯示放在了主線程處理。
上述代碼中,show的第二個(gè)參數(shù)fullScreen表示啟動(dòng)屏是全屏顯示(即是否隱藏狀態(tài)欄),代碼會(huì)控制對(duì)話框加載不同的主題樣式R.style.SplashScreen_Fullscreen與R.style.SplashScreen_SplashTheme來(lái)達(dá)到是否隱藏狀態(tài)的需求。
然后,我們可以在MainActivity.java的onCreate方法中調(diào)void show(final Activity activity,final boolean fullScreen)方法來(lái)顯示啟動(dòng)屏。
@Override
protected void onCreate(Bundle savedInstanceState) {
SplashScreen.show(this,true);
super.onCreate(savedInstanceState);
}
提示:
SplashScreen.show(this,true);放在super.onCreate(savedInstanceState);之前的位置效果會(huì)更好。
2.關(guān)閉對(duì)話框的方法:
/**
* 關(guān)閉啟動(dòng)屏
*/
public static void hide(Activity activity) {
if (activity == null) activity = mActivity.get();
if (activity == null) return;
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
if (mSplashDialog != null && mSplashDialog.isShowing()) {
mSplashDialog.dismiss();
}
}
});
}
上述代碼中,我們提供了關(guān)閉啟動(dòng)屏的方法。那么如何才能讓JS模塊調(diào)用void hide(Activity activity)來(lái)關(guān)閉啟動(dòng)屏呢?
第二步:向JS模塊提供SplashScreen組件
因?yàn)槲覀冃枰趈s中調(diào)用hide方法還控制啟動(dòng)屏的關(guān)閉。js不能直接調(diào)Java,所有我們需要為他們搭建一個(gè)橋梁(Native Modules)。
首先,創(chuàng)建一個(gè)ReactContextBaseJavaModule類(lèi)型的類(lèi),供js調(diào)用。
/**
* SplashScreenModule
* 出自:http://www.cboy.me
* GitHub:https://github.com/crazycodeboy
* Eamil:crazycodeboy@gmail.com
*/
public class SplashScreenModule extends ReactContextBaseJavaModule{
public SplashScreenModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "SplashScreen";
}
/**
* 打開(kāi)啟動(dòng)屏
*/
@ReactMethod
public void show() {
SplashScreen.show(getCurrentActivity());
}
/**
* 關(guān)閉啟動(dòng)屏
*/
@ReactMethod
public void hide() {
SplashScreen.hide(getCurrentActivity());
}
}
其次,創(chuàng)建一個(gè)ReactPackage類(lèi)型的類(lèi),用于向React Native注冊(cè)我們的SplashScreenModule組件。
/**
* SplashScreenReactPackage
* 出自:http://www.cboy.me
* GitHub:https://github.com/crazycodeboy
* Eamil:crazycodeboy@gmail.com
*/
public class SplashScreenReactPackage implements ReactPackage {
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new SplashScreenModule(reactContext));
return modules;
}
}
再次,在MainApplication中注冊(cè)SplashScreenModule組件。
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new SplashScreenReactPackage()
);
}
準(zhǔn)備工作,做好之后,下面我們就可以在JS中調(diào)用hide方法來(lái)關(guān)閉啟動(dòng)屏了。
第三步:在JS模塊中控制啟動(dòng)屏的關(guān)閉
創(chuàng)建一個(gè)名為SplashScreen的文件,加入下面代碼。
/**
* SplashScreen
* 啟動(dòng)屏
* 出自:http://www.cboy.me
* GitHub:https://github.com/crazycodeboy
* Eamil:crazycodeboy@gmail.com
* @flow
*/
'use strict';
import { NativeModules } from 'react-native';
module.exports = NativeModules.SplashScreen;
然后,我們可以在js中調(diào)用SplashScreen的hide()方法來(lái)關(guān)閉啟動(dòng)屏了。
componentDidMount() {
SplashScreen.hide();
}
不要忘記在使用SplashScreen的js文件中導(dǎo)入它哦
import SplashScreen from './SplashScreen。
iOS啟動(dòng)白屏解決方案
在iOS中,iOS支持為程序設(shè)置一個(gè)Launch Image或Launch Screen File來(lái)作為啟動(dòng)屏,當(dāng)程序被打開(kāi)的時(shí)候,首先顯示的便是設(shè)置的這個(gè)啟動(dòng)屏了。
那么小伙伴會(huì)問(wèn)了,這個(gè)啟動(dòng)屏幕什么時(shí)候會(huì)消失呢?
在AppDelegate如下方法:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
該方法返回一個(gè) BOOL類(lèi)型的值,當(dāng)系統(tǒng)調(diào)用該方并返回值之后,標(biāo)志著APP啟動(dòng)加載已經(jīng)完成,系統(tǒng)會(huì)將啟動(dòng)屏給關(guān)掉。
所以如果我們控制了這個(gè)啟動(dòng)屏幕讓它在js bundle加載并渲染完成之后再關(guān)閉不就解決了iOS 啟動(dòng)白屏了嗎?
上面已經(jīng)說(shuō)到,- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法執(zhí)行完成之后,啟動(dòng)屏?xí)魂P(guān)掉。
所以我們就想辦法控制該方實(shí)行的時(shí)間。
第一步:創(chuàng)建一個(gè)名為SplashScreen的Object-C文件
在SplashScreen.h文件中添加如下代碼:
//
// SplashScreen.h
// SplashScreen
// 出自:http://www.cboy.me
// GitHub:https://github.com/crazycodeboy
// Eamil:crazycodeboy@gmail.com
#import "RCTBridgeModule.h"
@interface SplashScreen : NSObject<RCTBridgeModule>
+ (void)show;
@end
在SplashScreen.m中添加如下代碼:
// SplashScreen
// 出自:http://www.cboy.me
// GitHub:https://github.com/crazycodeboy
// Eamil:crazycodeboy@gmail.com
#import "SplashScreen.h"
static bool waiting = true;
@implementation SplashScreen
- (dispatch_queue_t)methodQueue{
return dispatch_get_main_queue();
}
RCT_EXPORT_MODULE()
+ (void)show {
while (waiting) {
NSDate* later = [NSDate dateWithTimeIntervalSinceNow:0.1];
[[NSRunLoop mainRunLoop] runUntilDate:later];
}
}
RCT_EXPORT_METHOD(hide) {
dispatch_async(dispatch_get_main_queue(),
^{
waiting = false;
});
}
@end
在上述代碼中,我們通過(guò)[[NSRunLoop mainRunLoop] runUntilDate:later];來(lái)控制- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法執(zhí)行的時(shí)間,
主線程會(huì)每隔0.1s阻塞一次,直到waiting變量為true,然后我們就可以通過(guò)暴露給JS模塊的hide方法來(lái)控制waiting變量的值,繼而達(dá)到控制啟動(dòng)屏幕的關(guān)閉。
第二步:在JS模塊中控制啟動(dòng)屏的關(guān)閉
通過(guò)第一步我們已經(jīng)向JS模塊暴露了hide方法,然我們就可以在JS模塊中通過(guò)hide方法來(lái)關(guān)閉啟動(dòng)屏幕。
由于iOS在JS模塊中控制啟動(dòng)屏的關(guān)閉的方法和Android中第三步:在JS模塊中控制啟動(dòng)屏的關(guān)閉的方法是一樣的,這里就不再介紹了。
開(kāi)源庫(kù)
為了方便大家使用和解決React Native應(yīng)用啟動(dòng)白屏的問(wèn)題,我已經(jīng)將上述方案做成React Native組件react-native-splash-screen,
開(kāi)源在了GitHub上,小伙伴們可以下載使用。