Flutter学习系列(4)— 程序初始化

前面已经简单看过一个全新的Android Flutter应用程序的创建、编译以及安装包的结构。现在可以来看看Flutter程序是如何在Android上运行的。分析的应用程序是之前使用flutter create创建的Demo。功能就是点击界面上的【+】,然后界面上的数字递增显示。 我们只打开其中的Android项目。 flutter项目在最外层的lib/main.dart文件中。

 

 

一 Android项目结构

 

整个Demo的Android项目很简单,只有一个MainActivity页面和一个GeneratedPluginRegistrant文件。

看一下Activity中的代码, 这也太简单了,仅仅是调用了一下 GeneratedPluginRegistrant的方法, 没有任何UI相关的代码。

public class MainActivity extends FlutterActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    GeneratedPluginRegistrant.registerWith(this);
  }
}

GeneratedPluginRegistrant 也非常简单

/**
 * Generated file. Do not edit.
 */
public final class GeneratedPluginRegistrant {
  public static void registerWith(PluginRegistry registry) {
    if (alreadyRegisteredWith(registry)) {
      return;
    }
  }

  private static boolean alreadyRegisteredWith(PluginRegistry registry) {
    final String key = GeneratedPluginRegistrant.class.getCanonicalName();
    if (registry.hasPlugin(key)) {
      return true;
    }
    registry.registrarFor(key);
    return false;
  }
}

这个就是这个Demo的Android部分的全部代码? 看一下AndroidManifest.xml 有没有什么有用的东西?

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.cc.flutter.hello.flutter_hello">

    <!-- io.flutter.app.FlutterApplication is an android.app.Application that
         calls FlutterMain.startInitialization(this); in its onCreate method.
         In most cases you can leave this as-is, but you if you want to provide
         additional functionality it is fine to subclass or reimplement
         FlutterApplication and put your custom class here. -->
    <application
        android:name="io.flutter.app.FlutterApplication"
        android:label="flutter_hello"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- This keeps the window background of the activity showing
                 until Flutter renders its first frame. It can be removed if
                 there is no splash screen (such as the default splash screen
                 defined in @style/LaunchTheme). -->
            <meta-data
                android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
                android:value="true" />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>
</manifest>

原来指定了自定义的 FlutterApplication , 但是我们代码里并没有这个类,点击去看一下这个类所在的位置。

最终发现是在flutter.jar中,路径是:flutter/bin/cache/artifacts/engine/android-arm/flutter.jar。这个就是我们前面分析Flutter SDK时看到的flutter.jar,它的主要作用就是提供Android和Flutter Engine交互的桥梁,把Flutter App的容器嵌入到Android项目中, 以及完成的Engine的功能。所以我们可以通过分析它的源码来看一下Android和Flutter之间的一些基本的交互。

从整个Flutter应用的External Libraries中可以看到,引用到的库主要分为:

  1. Android Library: 这个是由Android SDK提供(android_sdk/platforms/android-25/android.jar),包括了Java和Android的功能的库。
  2. Dart Package: 这个是FlutterSDK提供的dart库,主要包括Flutter Framework (flutter/packages/flutter)负责UI相关的内容和 其他dart库(~/.pub-cache/hosted/pub.dartlang.org)。
  3. Dart SDK: 这个是Dart SDK提供的基础库,是语言语言层面的库。flutter/bin/cache/dart-sdk/lib
  4. Flutter for Android: 这个就是Flutter Engin提供的flutter.jar

 

这些库可以被自动引入是因为在Android Studio中安装了Flutter和Dart的插件,而Flutter可以被编译也是因为使用了Flutter SDK提供 gradle 插件。

apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

类似Gradle Plugin for Android,这个插件完成了编译Flutter程序以及和Android程序打包在一起的功能。所以整个打包流程我们是可以进行定制的。

 

 

 

二 Flutter Engine

 

Flutter.jar的源码并不在Flutter SDK中,而是在Flutter Engine的源码中。GitHub地址: https://github.com/flutter/engine

前面我们看过Flutter的架构,Engine是整个Flutter的核心部分, 我们可以看一下gihub上的介绍:

The Flutter Engine is a portable runtime for hosting Flutter applications. It implements Flutter’s core libraries, including animation and graphics, file and network I/O, accessibility support, plugin architecture, and a Dart runtime and compile toolchain. Most developers will interact with Flutter via the Flutter Framework, which provides a modern, reactive framework, and a rich set of platform, layout and foundation widgets.

Flutter Engine是使用C++编写的,最终会生成flutter.so 文件,类似Android的虚拟机 libart.so。正常来说开发一个Flutter直接和Flutter Framework打交到就可以了。 但是Flutter目前在Android或IOS上运行还是需要一个容器,所以Engine提供了一个jar包来完成这个功能(Shell)。另外Engine在不同平台上行为是一直的,因为引入了Embedded层来适配不同平台。

Flutter Engine的源码从git下载之后默认是master分支,看了一下不像Flutter SDK还有不同channel的分支,那么当前使用的Flutter SDK对应的是Engine那个提交的代码呢?

➜  io git:(3757390fa) ✗ flutter --version
Flutter 1.2.1 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 8661d8aecd (7 weeks ago) • 2019-02-14 19:19:53 -0800
Engine • revision 3757390fa4
Tools • Dart 2.1.2 (build 2.1.2-dev.0.0 0a7dcf17eb)

查看一下flutter的版本,标红的就是当前SDK使用的Engine的版本,使用这个revision可以直接切换到对应的commit (这个revision不是完整的commit ID,用了着么多年git我才知道可以用部分commit来切换,当然太短了就可能有重复了)

flutter_engine git:(master) ✗ git checkout 3757390fa4
HEAD is now at 3757390fa Roll src/third_party/dart ecd7a88606..0a7dcf17eb (4 commits)
flutter_engine git:(3757390fa) ✗ git log

commit 3757390fa4b00d2d261bfdf5182d2e87c9113ff9 (HEAD)
Author: Ben Konyi <bkonyi@google.com>
Date:   Wed Feb 13 13:45:39 2019 -0800

这样就成功切换到了SDK对应的Engine版本。如果想从Android Studio中通过jar直接查看到源码,点击Choose Sources..关联一下就可以了。关联之后就变成java文件了,这样分析源码就很方便了。

关联的代码位置: flutter_engine/shell/platform/android/io  (IOS的源码在platform/darwin 下)

 

 

 

三 程序初始化

 

对于Android程序来说,启动后最先执行的就是Application,这个Demo中,直接使用了FlutterApplication,内部调用了FlutterMain进行初始化。大多情况我们有自己的Application,这是可以继承FlutterApplication或则直接使用FlutterMain进行初始化。

/**
 * Flutter implementation of {@link android.app.Application}, managing
 * application-level global initializations.
 */
public class FlutterApplication extends Application {
    @Override
    @CallSuper
    public void onCreate() {
        super.onCreate();
        FlutterMain.startInitialization(this);
    }

    private Activity mCurrentActivity = null;
    public Activity getCurrentActivity() {
        return mCurrentActivity;
    }
    public void setCurrentActivity(Activity mCurrentActivity) {
        this.mCurrentActivity = mCurrentActivity;
    }
}

 

FlutterMain

 

FlutterMain这个class主要的作用就是初始化Flutter的Engine, 调用startInitialization方法进行初始化

   public static void startInitialization(Context applicationContext, Settings settings) {
        if (Looper.myLooper() != Looper.getMainLooper()) {
          throw new IllegalStateException("startInitialization must be called on the main thread");
        }
        // Do not run startInitialization more than once.
        if (sSettings != null) {
          return;
        }

        sSettings = settings;

        long initStartTimestampMillis = SystemClock.uptimeMillis();
        initConfig(applicationContext);
        initAot(applicationContext);
        initResources(applicationContext);
        System.loadLibrary("flutter");

        long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis;
        nativeRecordStartTimestamp(initTimeMillis);
    }

整个初始化比较清晰:

  1. 初始化必须在主线程进行,所以如果在Application中初始化可能会影响启动速度。
  2. 通过内部的Settings 变量来保证只进行一次初始化 。 (因为只能在主线程,所以也不需要加锁)
  3. 初始化操作主要包括对配置、AOT、资源
  4. 加载Flutter Engine的动态库flutter.so
  5. 对初始化进行计时

初始化的核心就在第三步,所以主要看一下这里面做的事情。

 

initConfig

 

主要是从程序的manifest文件中读取meta-data的配置,这些配置项主要是指定编译相关的一些信息,主页包括:

  • FlutterMain.aot-shared-library-path
  • FlutterMain.vm-snapshot-data
  • FlutterMain.vm-snapshot-instr
  • FlutterMain.isolate-snapshot-data
  • FlutterMain.isolate-snapshot-instr
  • FlutterMain.flx
  • FlutterMain.flutter-assets-dir

从名字就可以看出,名主要是指定编译后生成的vm和app代码文件的名字,已经assets目录名字。 下面是相关的默认值,我们之前在分析APK包结构的时候已经看过了。

String DEFAULT_AOT_SHARED_LIBRARY_PATH= "app.so";
String DEFAULT_AOT_VM_SNAPSHOT_DATA = "vm_snapshot_data";
String DEFAULT_AOT_VM_SNAPSHOT_INSTR = "vm_snapshot_instr";
String DEFAULT_AOT_ISOLATE_SNAPSHOT_DATA = "isolate_snapshot_data";
String DEFAULT_AOT_ISOLATE_SNAPSHOT_INSTR = "isolate_snapshot_instr";
String DEFAULT_FLX = "app.flx";
String DEFAULT_KERNEL_BLOB = "kernel_blob.bin";
String DEFAULT_FLUTTER_ASSETS_DIR = "flutter_assets";

 

initAot

 

说是初始化,从代码看其实是检查APK包中 aot文件是否完整。

  private static void initAot(Context applicationContext) {
        Set<String> assets = listAssets(applicationContext, "");
        sIsPrecompiledAsBlobs = assets.containsAll(Arrays.asList(
            sAotVmSnapshotData,
            sAotVmSnapshotInstr,
            sAotIsolateSnapshotData,
            sAotIsolateSnapshotInstr
        ));
        sIsPrecompiledAsSharedLibrary = assets.contains(sAotSharedLibraryPath);
        if (sIsPrecompiledAsBlobs && sIsPrecompiledAsSharedLibrary) {
          throw new RuntimeException(
              "Found precompiled app as shared library and as Dart VM snapshots.");
        }
    }
  1. listAssets 列举出APK包中asset目录下的所有文件
  2. 检查VM和Isolate相关的AOT Snapshot文件 (data和instr)是否在列表中
  3. 检查是否包含app.so
  4. 最后Flutter不允许同时有AOT文件和app.so

这里解释一些,在使用flutter build命令编译的时候,对于Android有一个选项, 这个可以利用NDK直接把flutter程序编译成一个so。

--build-shared-library    Whether to prefer compiling to a *.so file (android only).

 

initResources

 

这个方法的主要作用是检查当前app目录下的Flutter应用的文件版本和APK中的是否一致,如果不一致会使用APK中的资源文件来更新本地的。整个初始化过程主要是使用了3个类, 这3个类内部都是使用了AsyncTask执行相关操作:

  • ResourceCleaner:这个类是用来清理程序关闭时,没有清理的无用资源。主要是清理Cache目录下以.org.chromium.Chromium.开头的文件,这个动作会延迟5S执行。

  • ResourceUpdater:这个类提供了动态更新资源文件的功能,可以在meta-data中进行设置,开启动态更新功能并指定下载的path包的地址。下载的文件在Files目录下patch.download,下载完成后会重命名为patch.install。 meta-data包括:

    •  DynamicPatching 设置为true是开启动态更新功能
    •  PatchServerURL 设置patch所在的url, 最终下载的Uri =  PatchServerURL / appVersion.zip
    •  PatchDownloadMode 指定下载模式,包括ON_RESTARTON_RESUME,表示是APP启动时下载还是onResume时下载。
    •  PatchInstallMode 指定安装模式,包括ON_NEXT_RESTARTIMMEDIATE,表示是下次启动安装还是立即安装
  • ResourceExtractor:从名字看这个类的主要作用是从APK中解压出Flutter相关的资源文件到本地。前面下载patch时还有一个逻辑就是,如果是立即安装模式,需要等待下载完成,才开始执行ResourceExtractor。下面重点看一下ResourceExtractor的功能。

 

在开始执行之前,先添加相关的资源到ResourceExtractor中。 会发现vm和isolate的文件被添加了2次。这里主要是处理Debug和Release包,前面我们看APK包结构的时候发现debug所有文件都在flutter_assets目录下,所以这里会使用fromFlutterAssets, 全部保存在ResourceExtractor中的mResources列表中。

sResourceExtractor = new ResourceExtractor(context);

        sResourceExtractor
            .addResource(fromFlutterAssets(sFlx))
            .addResource(fromFlutterAssets(sAotVmSnapshotData))
            .addResource(fromFlutterAssets(sAotVmSnapshotInstr))
            .addResource(fromFlutterAssets(sAotIsolateSnapshotData))
            .addResource(fromFlutterAssets(sAotIsolateSnapshotInstr))
            .addResource(fromFlutterAssets(DEFAULT_KERNEL_BLOB));

        if (sIsPrecompiledAsSharedLibrary) {
          sResourceExtractor
            .addResource(sAotSharedLibraryPath);

        } else {
          sResourceExtractor
            .addResource(sAotVmSnapshotData)
            .addResource(sAotVmSnapshotInstr)
            .addResource(sAotIsolateSnapshotData)
            .addResource(sAotIsolateSnapshotInstr);
        }

        sResourceExtractor.start();

看一下里面的AsyncTask具体干了什么:

    private class ExtractTask extends AsyncTask<Void, Void, Void> {
        ExtractTask() { }

        @Override
        protected Void doInBackground(Void... unused) {
            final File dataDir = new File(PathUtils.getDataDirectory(mContext));

            ResourceUpdater resourceUpdater = FlutterMain.getResourceUpdater();
            if (resourceUpdater != null) {
                // Protect patch file from being overwritten by downloader while
                // it's being extracted since downloading happens asynchronously.
                resourceUpdater.getInstallationLock().lock();
            }

            try {
                if (resourceUpdater != null) {
                    File updateFile = resourceUpdater.getDownloadedPatch();
                    File activeFile = resourceUpdater.getInstalledPatch();

                    if (updateFile.exists()) {
                        JSONObject manifest = resourceUpdater.readManifest(updateFile);
                        if (resourceUpdater.validateManifest(manifest)) {
                            // Graduate patch file as active for asset manager.
                            if (activeFile.exists() && !activeFile.delete()) {
                                Log.w(TAG, "Could not delete file " + activeFile);
                                return null;
                            }
                            if (!updateFile.renameTo(activeFile)) {
                                Log.w(TAG, "Could not create file " + activeFile);
                                return null;
                            }
                        }
                    }
                }

                final String timestamp = checkTimestamp(dataDir);
                if (timestamp == null) {
                    return null;
                }

                deleteFiles();

                if (!extractUpdate(dataDir)) {
                    return null;
                }

                if (!extractAPK(dataDir)) {
                    return null;
                }

                if (timestamp != null) {
                    try {
                        new File(dataDir, timestamp).createNewFile();
                    } catch (IOException e) {
                        Log.w(TAG, "Failed to write resource timestamp");
                    }
                }

                return null;

            } finally {
              if (resourceUpdater != null) {
                  resourceUpdater.getInstallationLock().unlock();
              }
          }
        }
    }

代码主要操作如下:

  1. 检查是否有patch.install安装文件,然后从安装文件中读取manifest.json文件并解析。
  2. 调用validateManifest方法验证这个patch包是否适用于当前的APK。
    • buildNumber 表示patch对应的APK版本
    • baselineChecksum 表示isolate_snapshot_data、isolate_snapshot_instr、flutter_assets/isolate_snapshot_data 这3个文件的CRC32的值,会对比APK的asset目录下的文件和patch中的这个值是否一致
  3. 如果patch.install文件 有效,会重命名为patch.zip
  4. checkTimestamp方法
      1. 获取一下当前APK的版本和最后更新时间,组成一个timestamp
        String expectedTimestamp = TIMESTAMP_PREFIX + getVersionCode(packageInfo) + "-" + packageInfo.lastUpdateTime;
      2. 从patch.zip中读取manifest.json文件并解析,读取patchNumber字段,并和前面的timestamp拼接
        if (patchNumber != null) {
            expectedTimestamp += "-" + patchNumber + "-" + patchFile.lastModified();
        } else {
            expectedTimestamp += "-" + patchFile.lastModified();
        }
      3. 从本地flutter目录下查找 res_timestamp- 开头的文件,获取文件名列表
      4. 如果本地没有,或者有多个文件(正常只会有一个),或者文件名和expectedTimestamp不匹配返回expectedTimestamp,否则返回null表示不需要更新
  5. 如果有返回expectedTimestamp,说明需要更新本地资源
    1. 删除本地flutter目录下的所有前面添加到mResources列表中的文件(instr和data)
    2. 删除本地flutter目录下的所有res_timestamp- 开头的文件
  6. 如果patch.zip文件存在,解压zip文件,遍历mResources文件,从patch.zip中查找是否存在
    1. 如果存在拷贝到本地flutter目录下
    2. 如果不存在,查找是否存在 同名.bzdiff40的文件,存在的话利用BSDiff.bspatch根据APK中的资源文件和diff文件生成最终的资源文件拷贝到flutter目录下
  7. 从APK的assets中解压出资源文件,当本地不存在是才拷贝(以免覆盖上一步和patch生成的文件)
  8. 全部操作完成后,把前面获取的expectedTimestamp 作为文件名创建一个文件,标记本次操作

 

 

APK和本地文件

 

到此为止,一个Flutter应用在Application启动是的操作就分析完了,整体很简单,就是把APK中的flutter资源解压到本地,然后支持动态更新。安装一个debug的flutter包到手机运行一下,然后看一下:

和我们前面代码分析的一样,APK包中的flutter assets(和代码相关的资源)都被拷贝到了本地,并且生成了一个timestamp文件。如果开开启了动态更新,当下载了patch.zip后会重新更新到本地。如果APK重新安装了也会重新拷贝。

 final File dataDir = new File(PathUtils.getDataDirectory(mContext));

 public static String getDataDirectory(Context applicationContext) {
        return applicationContext.getDir("flutter", Context.MODE_PRIVATE).getPath();
    }

Flutter代码相关的数据都存放在本地flutter目录下,要注意这里使用getDir,系统会自动加上app_前缀,所以我们看到实际是存放在app_flutter目录下。

 

 

 

四 总结

 

这一篇主要介绍了Flutter的Android程序在启动时如何初始化Flutter相关资源的, 主要由FlutterMain来负责,相对比较简单。最主要代码是动态更新那一块,中使用了BSDiff进行差异更新。在实际开发过程中实际可以根据实际情况来重写这一块代码来完成热更新的功能。不过因为目前都是AOT代码,Release包没法做到HotReload,但是重启更新也提供了热更新的能力。

 


如果本文对您有帮助,可以扫描下方二维码打赏!您的支持是我的动力!
微信打赏 支付宝打赏

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注