Flutter&Android 启动页(闪屏页)的加载流程和优化方案

5,103 阅读5分钟

前言

现在应用的普遍启动方式为:

静态页面 -> 动图 -> 应用首页

之所以这样设计的原因,大致如下:

1、产品需求,如广告

2、品牌展示

3、应用规模较大时启动时间较长,相较于白屏,一张图片的过渡效果更好

等等...。

而Flutter由于引擎的创建和初始化需要一定时间,所以也提供了一个过渡方案(默认是白屏),位置在:

AndroidManifest.xml下的
<meta-data
          android:name="io.flutter.embedding.android.SplashScreenDrawable"
          android:resource="@drawable/launch_background"
 />
 

即res下的drawable/launch_background.xml 文件

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/red" />

<!-- You can insert your own image assets here -->
<!-- <item>
    <bitmap
        android:gravity="center"
        android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

我们可以在这里设置引擎初始化前所显示的页面,下面我们来看一下这个过程。

启动页的加载过程

我们这里只分析启动页的加载流程,引擎启动等流程将会略过。

MainActivity

整个flutter引擎的相关初始化工作在onCreate方法里开始的:


  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    switchLaunchThemeForNormalTheme();

    super.onCreate(savedInstanceState);

    lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);

    delegate = new FlutterActivityAndFragmentDelegate(this);
    ///创建绑定引擎等
    delegate.onAttach(this);
    ///用于插件、框架恢复状态
    delegate.onActivityCreated(savedInstanceState);
	///设置窗口背景透明,隐藏 status bar
    configureWindowForTransparency();
    ///这里是咱们的入口
    setContentView(createFlutterView());
    configureStatusBarForFullscreenFlutterExperience();
  }

setContentView大家很熟悉,我们直接看createFlutterView() 这个方法:

  @NonNull
  private View createFlutterView() {
    return delegate.onCreateView(
        null /* inflater */, null /* container */, null /* savedInstanceState */);
  }

FlutterActivityAndFragmentDelegate

flutter的初始化、启动等操作都是委托给它的。

我们继续看onCreateView,我将说明以注释的形式写在代码里

  @NonNull
  View onCreateView(
      LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    Log.v(TAG, "Creating FlutterView.");
    ensureAlive();
    
    if (host.getRenderMode() == RenderMode.surface) {
    /// flutter 应用在surface上显示,所以会进入到这里
      FlutterSurfaceView flutterSurfaceView =
          new FlutterSurfaceView(
              host.getActivity(), host.getTransparencyMode() == TransparencyMode.transparent);

      // Allow our host to customize FlutterSurfaceView, if desired.
      host.onFlutterSurfaceViewCreated(flutterSurfaceView);

      // Create the FlutterView that owns the FlutterSurfaceView.
      ///用我们的flutterSurfaceView 初始化了一个 FlutterView
      flutterView = new FlutterView(host.getActivity(), flutterSurfaceView);
    } else {
      FlutterTextureView flutterTextureView = new FlutterTextureView(host.getActivity());

      // Allow our host to customize FlutterSurfaceView, if desired.
      host.onFlutterTextureViewCreated(flutterTextureView);

      // Create the FlutterView that owns the FlutterTextureView.
      flutterView = new FlutterView(host.getActivity(), flutterTextureView);
    }

    // Add listener to be notified when Flutter renders its first frame.
    flutterView.addOnFirstFrameRenderedListener(flutterUiDisplayListener);
    
	/// 创建一个闪屏view - FlutterSplashView
    flutterSplashView = new FlutterSplashView(host.getContext());
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
      flutterSplashView.setId(View.generateViewId());
    } else {
      // TODO(mattcarroll): Find a better solution to this ID. This is a random, static ID.
      // It might conflict with other Views, and it means that only a single FlutterSplashView
      // can exist in a View hierarchy at one time.
      flutterSplashView.setId(486947586);
    }
    
    /// 显示闪屏页
    flutterSplashView.displayFlutterViewWithSplash(flutterView, host.provideSplashScreen());

    Log.v(TAG, "Attaching FlutterEngine to FlutterView.");
    ///所创建surface 绑定到engine上
    flutterView.attachToFlutterEngine(flutterEngine);

    return flutterSplashView;
  }

这里我们可以大致了解到,创建了一个FlutterSurfaceView 它继承自surfaceView(我们的flutter页面也是渲染在这个surface上的)。之后我们用它初始化一个FlutterView,

FlutterView继承自 FrameLayout

随后我们再创建一个FlutterSplashView (继承FrameLayout)并调用displayFlutterViewWithSplash()方法。

  public void displayFlutterViewWithSplash(
      @NonNull FlutterView flutterView, @Nullable SplashScreen splashScreen) {
    // If we were displaying a previous FlutterView, remove it.
    if (this.flutterView != null) {
      this.flutterView.removeOnFirstFrameRenderedListener(flutterUiDisplayListener);
      removeView(this.flutterView);
    }
    // If we were displaying a previous splash screen View, remove it.
    if (splashScreenView != null) {
      removeView(splashScreenView);
    }

    // Display the new FlutterView.
    this.flutterView = flutterView;
    ///添加flutterView
    addView(flutterView);

    this.splashScreen = splashScreen;

    // Display the new splash screen, if needed.
    if (splashScreen != null) {
      if (isSplashScreenNeededNow()) {
        Log.v(TAG, "Showing splash screen UI.");
        // This is the typical case. A FlutterEngine is attached to the FlutterView and we're
        // waiting for the first frame to render. Show a splash UI until that happens.
        splashScreenView = splashScreen.createSplashView(getContext(), splashScreenState);
        ///添加 splashScreenView 
        addView(this.splashScreenView);
        flutterView.addOnFirstFrameRenderedListener(flutterUiDisplayListener);
      } else if (isSplashScreenTransitionNeededNow()) {
        Log.v(
            TAG,
            "Showing an immediate splash transition to Flutter due to previously interrupted transition.");
        splashScreenView = splashScreen.createSplashView(getContext(), splashScreenState);
        addView(splashScreenView);
        transitionToFlutter();
      } else if (!flutterView.isAttachedToFlutterEngine()) {
        Log.v(
            TAG,
            "FlutterView is not yet attached to a FlutterEngine. Showing nothing until a FlutterEngine is attached.");
        flutterView.addFlutterEngineAttachmentListener(flutterEngineAttachmentListener);
      }
    }
  }

这个方法对flutterView进行了保存(不用管这个),随后我们保存了一个 接口的实现类——splashScreen,这个实现类则是由FlutterActivity实现的(MainActivity)来提供的:

///host 是个接口,由FlutterActivity实现
回顾上面:flutterSplashView.displayFlutterViewWithSplash(flutterView, host.provideSplashScreen());
  public SplashScreen provideSplashScreen() {
    Drawable manifestSplashDrawable = getSplashScreenFromManifest();
    if (manifestSplashDrawable != null) {
    ///DrawableSplashScreen 实现了 splashScreen的接口
      return new DrawableSplashScreen(manifestSplashDrawable);
    } else {
      return null;
    }
  }

通过getSplashScreenFromManifest 初始化了一个drawable,我们看一下它内部:

  @Nullable
  @SuppressWarnings("deprecation")
  private Drawable getSplashScreenFromManifest() {
    try {
      ActivityInfo activityInfo =
          getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
      Bundle metadata = activityInfo.metaData;
      ///这里就是我们在 AndroidManifest.xml中设置启动页了
      ///SPLASH_SCREEN_META_DATA_KEY 的值 见下方
      int splashScreenId = metadata != null ? metadata.getInt(SPLASH_SCREEN_META_DATA_KEY) : 0;
      return splashScreenId != 0
          ? Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP
              ? getResources().getDrawable(splashScreenId, getTheme())
              : getResources().getDrawable(splashScreenId)
          : null;
    } catch (PackageManager.NameNotFoundException e) {
      // This is never expected to happen.
      return null;
    }
  }
static final String SPLASH_SCREEN_META_DATA_KEY =
      "io.flutter.embedding.android.SplashScreenDrawable";

可以看到,通过上面的方法,我们以在AndroidManifest.xml设置的启动资源 初始化了一个drawable,进而初始化了DrawableSplashScreen 并返回。

我们回到displayFlutterViewWithSplash 继续向下看,

		///通过我们之前的drawable 生成一个 view
        splashScreenView = splashScreen.createSplashView(getContext(), splashScreenState);
        ///添加 splashScreenView 
        addView(this.splashScreenView);

到了这里,整个闪屏页的流程就跑完了。

回头看去,可以发现在闪屏页的显示到引擎的启动及flutter 页面的显示会有一个很长的过程,而直到flutter 页面的显示,这个闪屏页才会被移除掉。

那么如何优化呢? 以下是我的方案。

启动页优化方案

因为flutter与原生处于两个不同的surface,所以我们可以考虑利用原生的surface来显示这个启动页,同时为了避免拖慢flutter的启动,我们可以在子线程来做这个事情。ww

案例中我们显示一个svga动画
插件使用:'com.github.yyued:SVGAPlayer-Android:2.5.12'

实现

直接开工,我们写个方法initSplashPage() :

    public void initSplashPage(){
    Executors.newSingleThreadExecutor()
                .execute(new Runnable() {
            @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
            @Override
            public void run() {
            	///涉及到view的更新,所以我们需要一个loop
                Looper.prepare();
          
                WindowManager wm = (WindowManager)getSystemService(WINDOW_SERVICE);
                ///创建启动页的view
                SVGAImageView imageView = new SVGAImageView(getApplicationContext());
                imageView.setBackgroundColor(getResources().getColor(R.color.white));
                
                Handler handler = new Handler(){
                    @Override
                    public void handleMessage(Message msg) {
                        switch (msg.what){
                            case 1111:
                                ///在启动页中 显示 svga动画
                                imageView.setImageDrawable((SVGADrawable)msg.obj);
                                imageView.startAnimation();
                                break;
                        }
                    }
                };
                
                ///解析 assets的svga资源
                SVGAParser parser = new SVGAParser(getApplicationContext());
                parser.decodeFromAssets("angel.svga", new SVGAParser.ParseCompletion() {
                    @Override
                    public void onComplete(SVGAVideoEntity svgaVideoEntity) {
                    
                    	///由于 插件内部切换了主线程,所以我们这里需要handler再次切换
                        SVGADrawable drawable = new SVGADrawable(svgaVideoEntity);
                        Message msg = Message.obtain();
                        msg.what = 1111;
                        msg.obj = drawable;
                        handler.sendMessage(msg);
                    }

                    @Override
                    public void onError() {

                    }
                });
                
                WindowManager.LayoutParams params = new WindowManager.LayoutParams();
                params.width = wm.getDefaultDisplay().getWidth();
                params.height = wm.getDefaultDisplay().getHeight();
				///显示闪屏页
                wm.addView(imageView, params);
                
                ///6秒后去掉闪屏页,显示flutter页面
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        wm.removeView(imageView);
                    }
                }, 6000);
                

                Looper.loop();


            }
        });
        
    }

以上是整个实现过程,由于svga插件的解析回调在主线程,所以需要通过子线程的handler进行一个线程切换,最后通过wm进行上屏显示。

另外,我额外在6秒后移除了闪屏页,实际上应该由flutter端通过channel,来通知移除这个闪屏页,我这里写的随意了一些。

使用

之后我们在MainActivity的onCreate中使用它:

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        initSplashPage();
        ///flutter相关的工作
        super.onCreate(savedInstanceState);
    }

效果如下:

为了查看效果,额外设置了一些颜色
红色为       AndroidMainfest.xml中设置的闪屏页
蓝色为	    flutter 页面
动图为       咱们所显示的闪屏页

到此文章告一段落,如果大家有更好的方案,可以一起交流探讨,谢谢阅读。

Demo

Demo 做了一些实验,所以代码比较乱,嘿嘿

系列文章

Flutter 仿网易云音乐App

Flutter版 仿.知乎列表的视差效果

Flutter——实现网易云音乐的渐进式卡片切换

Flutter 仿同花顺自选股列表