React Native Android的启动白屏/闪屏的原因,解决方案,原理,使用方法

hply3206 8年前
   <h2>问题描述:</h2>    <p>用React Native架构的无论是Android APP还是iOS APP,在启动时都出现白屏现象,时间大概1~3s(根据手机或模拟器的性能不同而不同)。</p>    <h2>问题分析:</h2>    <p>React Native应用在启动时会将js bundle读取到内存中,并完成渲染。这期间由于js bundle还没有完成装载并渲染,所以界面显示的是白屏。</p>    <p>白屏给人的感觉很不友好,那有没有办法不显示白屏呢?</p>    <p>上文解释了:为什么React Native应用会在启动的时候显示一会白屏。既然知道了出现问题的原因,那么离解决问题也不远了。市场上大部分APP在启动的时候都会有个启动屏,启动屏对于用户是比较友好的,一来展示欢迎信息,二来显示一些产品信息或一些广告,启动页对于程序来说,是为程序完成初始化加载数据,做一些初始化工作的所保留的时间,启动屏等待的时间可长可短,具体根据业务而定。</p>    <p>下面我就教大家如何给React Native Android加启动屏,并解决启动白屏的问题。</p>    <h2>为React Native Android添加启动屏(解决白屏等待问题)</h2>    <p>为了实现为React Native Android添加启动屏,我们需要给React Native动刀了了。下面就让我们从源码看起。</p>    <h3>原理分析</h3>    <p>通过 react-native init <project name> 初始化的应用,Android部分,只有一个 MainActivity ,它是整个Android程序的入口。</p>    <pre>  <code class="language-java">public class MainActivity extends ReactActivity {      /**       * Returns the name of the main component registered from JavaScript.       * This is used to schedule rendering of the component.       */      @Override      protected String getMainComponentName() {          return "GitHubPopular";      }  }</code></pre>    <p>通过上述代码可以看出 MainActivity 很干净,就一个 getMainComponentName() 方法。显然启动白屏不是因为 MainActivity 导致的。</p>    <p>接下来,我们就继续探索,进入 ReactActivity 源码一探究竟。</p>    <pre>  <code class="language-java">@Override    protected void onCreate(Bundle savedInstanceState) {      super.onCreate(savedInstanceState);      if (getUseDeveloperSupport() && Build.VERSION.SDK_INT >= 23) {        // Get permission to show redbox in dev builds.        if (!Settings.canDrawOverlays(this)) {          Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);          startActivity(serviceIntent);          FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);          Toast.makeText(this, REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();        }      }      mReactRootView = createRootView();      mReactRootView.startReactApplication(        getReactNativeHost().getReactInstanceManager(),        getMainComponentName(),        getLaunchOptions());      setContentView(mReactRootView);      mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();    }</code></pre>    <p>上面代码是 ReactActivity 的 onCreate 方法的代码, onCreate 作为一个Activity的入口,负责着程序初始化等一系列工作。</p>    <p>熟悉Android开发的小伙伴都知道,在 onCreate 方法通过 setContentView() 方法设置一个用于用户交互界面。在 ReactActivity 的 onCreate 方法中也有使用 setContentView() 。</p>    <pre>  <code class="language-java">mReactRootView = createRootView();     mReactRootView.startReactApplication(        getReactNativeHost().getReactInstanceManager(),        getMainComponentName(),        getLaunchOptions());   setContentView(mReactRootView);</code></pre>    <p>上述代码中,首先通过 mReactRootView = createRootView(); 创建一个根视图,该视图便是React Native应用的最顶部视图。然后通过 mReactRootView.startReactApplication 方法,加载并渲染js bundle,此过程是比较耗时的。最后,通过 setContentView(mReactRootView); 将根视图绑定到Activity界面上。</p>    <p>基本原理就是这些,下面我们就对 ReactActivity 动动刀子。</p>    <h2>实现思路</h2>    <p>先说一下思路:</p>    <ol>     <li>APP启动的时候控制ReactActivity显示启动屏。</li>     <li>提供关闭启动屏的公共接口。</li>     <li>在js的适当位(一般是程序初始化工作完成后)置调用上述公共接口关闭启动屏。</li>    </ol>    <h2>具体实现</h2>    <h3>第一步:APP启动的时候控制ReactActivity显示启动屏</h3>    <p>在给 ReactActivity 动刀子前我们需要进行一些准备工作。</p>    <p>基础准备:</p>    <p>首先,我们需要将 ReactActivity 复制一份出来。</p>    <p>因为 ReactActivity 是React Native源码中的一部分,我们无法直接对其源码进行修改,所以我们需将它复制一份出来。然后将 MainActivity 继承改为我们复制出来的这个 ReactActivity 。</p>    <p>其次。修改getUseDeveloperSupport方法。</p>    <p>因为, ReactNativeHost 的 getUseDeveloperSupport 方法是受保护类型的,所以我们无法在它所属包之外访问该方法。但我们又需要在 ReactActivity 中调用该方法,那么我们可以使用反射来满足我们这一需求。</p>    <pre>  <code class="language-java">protected boolean getUseDeveloperSupport() {      ReactNativeHost rnh=((ReactApplication) getApplication()).getReactNativeHost();          Class<?>cls=rnh.getClass();      Object support= null;      try {          Method method = cls.getDeclaredMethod("getUseDeveloperSupport", new Class[]{});          method.setAccessible(true);          support=method.invoke(rnh);      } catch (NoSuchMethodException e) {          e.printStackTrace();      } catch (InvocationTargetException e) {          e.printStackTrace();      } catch (IllegalAccessException e) {          e.printStackTrace();      }      return (boolean) support;  }</code></pre>    <p>前期工作准备玩了,现在让我们开始吧。</p>    <p>为了让ReactActivity显示启动屏我们需要创建一个View容器,来容纳启动屏视图和React Native根视图。</p>    <pre>  <code class="language-java">@Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);            if (getUseDeveloperSupport() && Build.VERSION.SDK_INT >= 23) {              // Get permission to show redbox in dev builds.              if (!Settings.canDrawOverlays(this)) {                  Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);                  startActivity(serviceIntent);                  FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);                  Toast.makeText(this, REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();              }          }            mRootView = new FrameLayout(this);          splashView = LayoutInflater.from(this).inflate(R.layout.launch_screen, null);            mReactRootView = createRootView();          mReactRootView.startReactApplication(                  getReactNativeHost().getReactInstanceManager(),                  getMainComponentName(),                  getLaunchOptions());          mRootView.addView(mReactRootView);          mRootView.addView(splashView, new ViewGroup.LayoutParams(                  ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));          setContentView(mRootView);          mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();      }</code></pre>    <p>首先,我创建了一个 mRootView = new FrameLayout(this); 视图容器。</p>    <p>其次,将启动屏布局文件读到内存中 splashView = LayoutInflater.from(this).inflate(R.layout.launch_screen, null); 。</p>    <p>再次,添加 mReactRootView 与 splashView ,注意添加顺序。</p>    <p>最后,将 mRootView 绑定到Activity。</p>    <p>这样一来,我们就控制了ReactActivity在启动的时候显示欢迎界面。下面我们需要让ReactActivity开放关闭换用界面的接口方法。</p>    <pre>  <code class="language-java">/**   * 隐藏启动屏幕   */  public void hide() {      if (mRootView == null || splashView == null) return;      AlphaAnimation fadeOut = new AlphaAnimation(1, 0);      fadeOut.setDuration(1000);      splashView.startAnimation(fadeOut);      fadeOut.setAnimationListener(new Animation.AnimationListener() {          @Override          public void onAnimationStart(Animation animation) {          }            @Override          public void onAnimationEnd(Animation animation) {              mRootView.removeView(splashView);              splashView = null;          }            @Override          public void onAnimationRepeat(Animation animation) {          }      });  }</code></pre>    <p>上述方法,中加入了一个淡出动画持续1s,目的是让欢迎界面和其他界面之间过度自然些。</p>    <p>做到这里还不够,因为我们需要在js中调用 hide 方法还控制欢迎界面的关闭。js不能直接调Java,所有我们需要为他们搭建一个桥梁( <a href="/misc/goto?guid=4959715830126579166" rel="nofollow,noindex">Native Modules</a> )。</p>    <p>首先,创建一个 ReactContextBaseJavaModule 类型的类,供js调用。</p>    <pre>  <code class="language-java">/**   * LaunchScreenModule   * 出自:http://www.cboy.me   * GitHub:https://github.com/crazycodeboy   * Eamil:crazycodeboy@gmail.com   */  public class LaunchScreenModule extends ReactContextBaseJavaModule{      public LaunchScreenModule(ReactApplicationContext reactContext) {          super(reactContext);      }        @Override      public String getName() {          return "LaunchScreen";      }      @ReactMethod      public void hide(){          getCurrentActivity().runOnUiThread(new Runnable() {              @Override              public void run() {                  ((ReactActivity)getCurrentActivity()).hide();              }          });        }  }</code></pre>    <p>其次,创建一个 ReactPackage 类型的类,用于向React Native注册我们的 LaunchScreenModule 组件。</p>    <pre>  <code class="language-java">/**   * LaunchScreenReactPackage   * 出自:http://www.cboy.me   * GitHub:https://github.com/crazycodeboy   * Eamil:crazycodeboy@gmail.com   */  public class LaunchScreenReactPackage 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 LaunchScreenModule(reactContext));          return modules;      }  }</code></pre>    <p>再次,在MainApplication中注册 LaunchScreenModule 组件。</p>    <pre>  <code class="language-java">@Override  protected List<ReactPackage> getPackages() {      return Arrays.<ReactPackage>asList(              new MainReactPackage(),              new LaunchScreenReactPackage()      );  }</code></pre>    <p>最后,在js中调用LaunchScreenModule。</p>    <p>创建一个名为 LaunchScreen 的文件,加入下面代码。</p>    <pre>  <code class="language-java">/**   * LaunchScreen   * Android启动屏   * 出自:http://www.cboy.me   * GitHub:https://github.com/crazycodeboy   * Eamil:crazycodeboy@gmail.com   * @flow   */  'use strict';    import { NativeModules } from 'react-native';  module.exports = NativeModules.LaunchScreen;</code></pre>    <p>上述代码,目的是向js暴露 LaunchScreen 模块。</p>    <p>下面我们就可以在js中调用 LaunchScreen 的 hide() 方法来关闭启动屏了。</p>    <pre>  <code class="language-java">LaunchScreen.hide();</code></pre>    <p>不要忘记在使用LaunchScreen的js文件中导入它哦 import LaunchScreen from './LaunchScreen 。</p>    <p>到这里,React Native Android的启动白屏的原因,解决方案,原理,使用方法已经向大家介绍完了。大家如果还有什么疑问可以加群: 165774887 ,和我一起讨论。</p>    <p>另外,跟大家分享一个Android启动时闪现白屏或黑屏的解决方案。</p>    <p>这个问题是Android主题的问题和React Native无关,请往下看。</p>    <h2>修改主题解决闪现白屏/黑屏</h2>    <h3>问题描述:</h3>    <p>市场上有很多应用,在启动的时候,会出现闪现黑屏或白屏,有的应用却没有。究其原因,是主题在搞鬼。</p>    <h3>问题分析</h3>    <p>当单击应用的图标时,Android会为被单击的应用创建一个进程,然后创建一个Application实例,然后应用主题,然后启动Activity。</p>    <p>因为启动Activity也是需要时间的,这之间的时间间隔,便是闪现白屏或黑屏的时间。</p>    <h3>解决方案</h3>    <p>为解决启动时闪现白屏或黑屏的问题,我们可以从主题下手,为应用创建一个透明的主题。</p>    <p>第一步:创建一个透明主题。</p>    <pre>  <code class="language-java"><!-- Base application theme. -->  <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">          <!--设置透明背景-->      <item name="android:windowIsTranslucent">true</item>  </style></code></pre>    <p>第二步:在AndroidManifest.xml中为application应用主题。</p>    <pre>  <code class="language-java"><application        android:name=".MainApplication"        android:allowBackup="true"        android:label="@string/app_name"        android:icon="@mipmap/ic_launcher"        android:theme="@style/AppTheme"></code></pre>    <p>这样一来,启动时变不会闪现黑屏或白屏了。</p>    <p>如果,你的应用需要一个特定的主题,但该主题不是透明的,你可以先将application的默认主题设置成透明的主题,然后在程序启动后(可以在启动页进行),通过 public void setTheme(int resid) 方法将主题设置成你想要的主题即可。</p>    <h2> </h2>    <p>来自:http://www.jianshu.com/p/6917e536de27</p>    <p> </p>