問題描述:

用React Native架構的無論是Android APP還是iOS APP,在啟動時都出現白屏現象,時間大概1~3s(根據手機或模擬器的效能不同而不同)。

問題分析:

為什麼會產生白屏?

React Native應用在啟動時會將js bundle讀取到記憶體中,並完成渲染。這期間由於js bundle還沒有完成裝載並渲染,所以介面顯示的是白屏。

白屏給人的感覺很不友好,那有沒有辦法不顯示白屏呢?

上文解釋了:為什麼React Native應用會在啟動的時候顯示一會白屏。既然知道了出現問題的原因,那麼離解決問題也不遠了。市場上大部分APP在啟動的時候都會有個啟動屏,啟動屏對於使用者是比較友好的,一來展示歡迎資訊,二來顯示一些產品資訊或一些廣告,啟動頁對於程式來說,是為程式完成初始化載入資料,做一些初始化工作的所保留的時間,啟動屏等待的時間可長可短,具體根據業務而定。

下面我就教大家如何給React Native 應用新增啟動屏,並解決啟動白屏的問題。

Android啟動白屏解決方案

我們可以通過為React Native Android應用新增啟動屏的方式,來解決啟動白屏的問題。我在《React Native Android啟動屏,啟動白屏,閃現白屏》一文中介紹過一種為React Native Android應用新增啟動屏的方法, 不過那種方法雖好,但牽扯到對React Native 原始碼的修改,如果React Native 版本有更新還需要對原始碼做一些處理,所以以後維護起來不是很方便。

下面就向大家介紹另外一種為React Native Android應用新增啟動屏的方案。

《React Native Android啟動屏,啟動白屏,閃現白屏》一文中 我們使用的是在根檢視容器上新增一個檢視作為啟動屏,當js bundle載入並渲染完成後,再將新增的檢視從根檢視上移除。在根檢視上新增一個檢視的方式其實就是為了遮擋白屏,既然是遮擋白屏,我們是不是可以彈出一個對話方塊呢?

小夥伴們肯定會說,對話方塊也不是全屏啊,主題也不一樣啊,不過沒關係,既然我們可以新增對話方塊,那麼我們就可以修改對話方塊的樣式來達到我們需要的效果。

要達到啟動屏的效果,我們需要一個什麼樣效果的對話方塊呢?

  1. 在APP啟動的時候顯示;
  2. 在js bundle載入並渲染完成後消失;
  3. 全屏顯示;
  4. 顯示的內容可以通過 layout xml 進行修改;

上述是我們對這個對話方塊的基本需求,現在就讓我們來實現這一需求:

為滿足上述需求,對話方塊元件需要提供下面兩個方法:

1.顯示對話方塊的方法:

/**
 * 開啟啟動屏
 */
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被銷燬的時候,持有的Activity能被及時的回收,這裡我們通過new WeakReference<Activity>(activity);建立了一個Activity的弱引用。

另外,因為在Android中所有的有關UI操作都必須在主執行緒,所有我們通過activity.runOnUiThread(new Runnable()...,將對話方塊的顯示放在了主執行緒處理。

然後,我們可以在MainActivity.javaonCreate方法中調void show(final Activity activity,final boolean fullScreen)方法來顯示啟動屏。

@Override
protected void onCreate(Bundle savedInstanceState) {
    SplashScreen.show(this,true);
    super.onCreate(savedInstanceState);
}

提示:SplashScreen.show(this,true);放在super.onCreate(savedInstanceState);之前的位置效果會更好。

2.關閉對話方塊的方法:

/**
 * 關閉啟動屏
 */
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();
            }
        }
    });
}

上述程式碼中,我們提供了關閉啟動屏的方法。那麼如何才能讓JS模組呼叫void hide(Activity activity)來關閉啟動屏呢?

因為我們需要在js中呼叫hide方法還控制啟動屏的關閉。js不能直接調Java,所有我們需要為他們搭建一個橋樑(Native Modules)。

首先,建立一個ReactContextBaseJavaModule型別的類,供js呼叫。

/**
 * SplashScreenModule
 * 出自:http://www.devio.org
 * GitHub:https://github.com/crazycodeboy
 * Eamil:[email protected]
 */
public class SplashScreenModule extends ReactContextBaseJavaModule{
    public SplashScreenModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

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

    /**
     * 開啟啟動屏
     */
    @ReactMethod
    public void show() {
        SplashScreen.show(getCurrentActivity());
    }

    /**
     * 關閉啟動屏
     */
    @ReactMethod
    public void hide() {
        SplashScreen.hide(getCurrentActivity());
    }
}

其次,建立一個ReactPackage型別的類,用於向React Native註冊我們的SplashScreenModule元件。

/**
 * SplashScreenReactPackage
 * 出自:http://www.devio.org
 * GitHub:https://github.com/crazycodeboy
 * Eamil:[email protected]
 */
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中註冊SplashScreenModule元件。

@Override
protected List<ReactPackage> getPackages() {
    return Arrays.<ReactPackage>asList(
            new MainReactPackage(),
            new SplashScreenReactPackage()
    );
}

準備工作,做好之後,下面我們就可以在JS中呼叫hide方法來關閉啟動屏了。

第三步:在JS模組中控制啟動屏的關閉

/**
 * SplashScreen
 * 啟動屏
 * 出自:http://www.devio.org
 * GitHub:https://github.com/crazycodeboy
 * Eamil:[email protected]
 * @flow
 */
'use strict';

import { NativeModules } from 'react-native';
module.exports = NativeModules.SplashScreen;

然後,我們可以在js中呼叫SplashScreen的hide()方法來關閉啟動屏了。

componentDidMount() {
    SplashScreen.hide();
}

不要忘記在使用SplashScreen的js檔案中匯入它哦import SplashScreen from './SplashScreen

iOS啟動白屏解決方案

在iOS中,iOS支援為程式設定一個Launch Image或Launch Screen File來作為啟動屏,當程式被開啟的時候,首先顯示的便是設定的這個啟動屏了。

那麼小夥伴會問了,這個啟動螢幕什麼時候會消失呢?

AppDelegate如下方法:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

該方法返回一個 BOOL型別的值,當系統呼叫該方並返回值之後,標誌著APP啟動載入已經完成,系統會將啟動屏給關掉。

所以如果我們控制了這個啟動螢幕讓它在js bundle載入並渲染完成之後再關閉不就解決了iOS 啟動白屏了嗎?

上面已經說到,- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法執行完成之後,啟動屏會被關掉。

所以我們就想辦法控制該方實行的時間。

第一步:建立一個名為SplashScreen的Object-C檔案

在SplashScreen.h檔案中新增如下程式碼:

//
//  SplashScreen.h
//  SplashScreen
//  出自:http://www.devio.org
//  GitHub:https://github.com/crazycodeboy
//  Eamil:[email protected]


#import "RCTBridgeModule.h"

@interface SplashScreen : NSObject<RCTBridgeModule>
+ (void)show;
@end

在SplashScreen.m中新增如下程式碼:

//  SplashScreen
//  出自:http://www.devio.org
//  GitHub:https://github.com/crazycodeboy
//  Eamil:[email protected]

#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

在上述程式碼中,我們通過[[NSRunLoop mainRunLoop] runUntilDate:later];來控制- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法執行的時間, 主執行緒會每隔0.1s阻塞一次,直到waiting變數為true,然後我們就可以通過暴露給JS模組的hide方法來控制waiting變數的值,繼而達到控制啟動螢幕的關閉。

第二步:在JS模組中控制啟動屏的關閉

通過第一步我們已經向JS模組暴露了hide方法,然我們就可以在JS模組中通過hide方法來關閉啟動螢幕。 由於iOS在JS模組中控制啟動屏的關閉的方法和Android中第三步:在JS模組中控制啟動屏的關閉的方法是一樣的,這裡就不再介紹了。

開源庫

為了方便大家使用和解決React Native應用啟動白屏的問題,我已經將上述方案做成React Native元件react-native-splash-screen, 開源在了GitHub上,小夥伴們可以下載使用。