Flutter混合開(kāi)發(fā)和動(dòng)態(tài)更新的探索歷程 Android版

Flutter是Google推出的可以高效構(gòu)建Android菜职、iOS界面的移動(dòng)UI框架肴盏,在國(guó)內(nèi)中大公司像閑魚(yú)/Now直播等app陸續(xù)出現(xiàn)它的影子燕鸽,當(dāng)然閑魚(yú)的最為成熟闪彼,閑魚(yú)也非常的高效產(chǎn)出了很多優(yōu)秀的文章婿滓。

本文是基于Flutter SDK : 0.7.3
在最新的SDK v0.11.13中或者說(shuō)運(yùn)行后發(fā)現(xiàn)沒(méi)有PathProviderPlugin / SharedPreferencesPlugin 對(duì)應(yīng)的目錄以及jar包老速,那是因?yàn)樾掳姹局幸呀?jīng)不需要了 自然就可以刪除。

可是

可是凸主,網(wǎng)上能找到的混合開(kāi)發(fā)方案或者動(dòng)態(tài)更新flutter的相關(guān)文章都沒(méi)法符合我自己理想的效果橘券。所以自己摸索了一套混合開(kāi)發(fā)和動(dòng)態(tài)更新的方案,這里記錄一下摸索過(guò)程卿吐。

Flutter源碼分析

如果說(shuō)把自家的app改造成純Flutter方案那是不可能的旁舰,頂多是某個(gè)模塊或者某些模塊改成Flutter,所以自然想到Flutter如何跟原生混合開(kāi)發(fā)嗡官,混合開(kāi)發(fā)不是說(shuō)java去調(diào)用dart中的方法更多的是指如何從當(dāng)前Activity跳轉(zhuǎn)到Flutter實(shí)現(xiàn)的界面箭窜,要像知道這些東西那么必須得弄懂Flutter源碼,不求深入但求知之一二三四衍腥。
Android的應(yīng)用那么自然先找Application绽快,所以很快找到了FlutterApplication:

public class FlutterApplication extends Application {
    private Activity mCurrentActivity = null;

    public FlutterApplication() {
    }

    @CallSuper
    public void onCreate() {
        super.onCreate();
        FlutterMain.startInitialization(this);
    }

    public Activity getCurrentActivity() {
        return this.mCurrentActivity;
    }

    public void setCurrentActivity(Activity mCurrentActivity) {
        this.mCurrentActivity = mCurrentActivity;
    }
}

還行初始化的東西不多,直接進(jìn)入onCreate對(duì)應(yīng)的FlutterMain.startInitialization中去看看:

public static void startInitialization(Context applicationContext, FlutterMain.Settings settings) {
      long initStartTimestampMillis = SystemClock.uptimeMillis();
      initConfig(applicationContext);
      initAot(applicationContext);
      initResources(applicationContext);
      System.loadLibrary("flutter");
      long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis;
      nativeRecordStartTimestamp(initTimeMillis);
 }

不具體一行一行的看代碼紧阔,但是看到了幾個(gè)很關(guān)鍵的詞在initConfig方法中:

private static void initConfig(Context applicationContext) {
      Bundle metadata = applicationContext.getPackageManager().getApplicationInfo(applicationContext.getPackageName(), 128).metaData;
      if (metadata != null) {
          sAotSharedLibraryPath = metadata.getString(PUBLIC_AOT_AOT_SHARED_LIBRARY_PATH, "app.so");
          sAotVmSnapshotData = metadata.getString(PUBLIC_AOT_VM_SNAPSHOT_DATA_KEY, "vm_snapshot_data");
          sAotVmSnapshotInstr = metadata.getString(PUBLIC_AOT_VM_SNAPSHOT_INSTR_KEY, "vm_snapshot_instr");
          sAotIsolateSnapshotData = metadata.getString(PUBLIC_AOT_ISOLATE_SNAPSHOT_DATA_KEY, "isolate_snapshot_data");
          sAotIsolateSnapshotInstr = metadata.getString(PUBLIC_AOT_ISOLATE_SNAPSHOT_INSTR_KEY, "isolate_snapshot_instr");
          sFlx = metadata.getString(PUBLIC_FLX_KEY, "app.flx");
          sSnapshotBlob = metadata.getString(PUBLIC_SNAPSHOT_BLOB_KEY, "snapshot_blob.bin");
          sFlutterAssetsDir = metadata.getString(PUBLIC_FLUTTER_ASSETS_DIR_KEY, "flutter_assets");
      }
    }

沒(méi)錯(cuò)就是vm_snapshot_data坊罢、vm_snapshot_instr、isolate_snapshot_data擅耽、isolate_snapshot_instr為什么說(shuō)這幾個(gè)這么重要呢活孩?

在這里插入圖片描述

看下上面這幾個(gè)編譯的產(chǎn)物,我們就知道這就Flutter的核心東西乖仇『度澹或者換句話說(shuō)只要弄懂了這個(gè)玩意很有可能我們就悟出混合開(kāi)發(fā)的方案了询兴,那么他們是怎么讀取assets目錄下的這些玩意呢?

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.");
        }
    }

看到方法跟Assets掛鉤確實(shí)很驚喜起趾,因?yàn)榭吹娇隙ㄊ菑腁ssets中把這些讀出來(lái)的诗舰。可是讀出來(lái)放哪里去训裆?
那最后的那個(gè)方法initResources該方法就是涉及存放的位置眶根,跟著源碼一路看下去,在ExtractTask.extractResources找到了一點(diǎn)貓膩:

File dataDir = new File(PathUtils.getDataDirectory(ResourceExtractor.this.mContext));

確實(shí)边琉,就是在data/data/xxx/flutter_assets/路徑下:

在這里插入圖片描述

大體知道了這些個(gè)產(chǎn)物之后属百,界面是怎么加載? 首先加載Flutter的界面是個(gè)Activity叫FlutterActivity主要是通過(guò)FlutterActivityDelegate這個(gè)類(lèi)变姨,然后我們主要看FlutterActivity.onCreate => FlutterActivityDelegate.onCreate這個(gè)流程:

public void onCreate(Bundle savedInstanceState) {
    // 沉浸式模式
        if (VERSION.SDK_INT >= 21) {
            Window window = this.activity.getWindow();
            window.addFlags(-2147483648);
            window.setStatusBarColor(1073741824);
            window.getDecorView().setSystemUiVisibility(1280);
        }

        String[] args = getArgsFromIntent(this.activity.getIntent());
        FlutterMain.ensureInitializationComplete(this.activity.getApplicationContext(), args);
        this.flutterView = this.viewFactory.createFlutterView(this.activity);
        if (this.flutterView == null) {
            FlutterNativeView nativeView = this.viewFactory.createFlutterNativeView();
            this.flutterView = new FlutterView(this.activity, (AttributeSet)null, nativeView);
            this.flutterView.setLayoutParams(matchParent);
            this.activity.setContentView(this.flutterView);
            this.launchView = this.createLaunchView();
            if (this.launchView != null) {
                this.addLaunchView();
            }
        }
    }

所以界面最重要的方法就是ensureInitializationComplete也就是把flutter相關(guān)的初始化進(jìn)來(lái)然后使用FlutterView進(jìn)行加載顯示:

ensureInitializationComplete:// 進(jìn)行初始化
String appBundlePath = findAppBundlePath(applicationContext);
String appStoragePath = PathUtils.getFilesDir(applicationContext);
nativeInit(applicationContext, (String[])shellArgs.toArray(new String[0]), appBundlePath, appStoragePath);

// 找到data/data/xxx/flutter_assets下的flutter產(chǎn)物
public static String findAppBundlePath(Context applicationContext) {
    String dataDirectory = PathUtils.getDataDirectory(applicationContext);
    File appBundle = new File(dataDirectory, sFlutterAssetsDir);
    return appBundle.exists() ? appBundle.getPath() : null;
}

然后每一個(gè)FlutterView中包了一個(gè)FlutterNativeView然后最終就是FlutterView->runFromBundle調(diào)用FlutterNativeView->runFromBundle最后渲染到界面上族扰。
到此我們大概了解了Flutter需要的產(chǎn)物vm_snapshot_data、vm_snapshot_instr定欧、isolate_snapshot_data渔呵、isolate_snapshot_instr然后簡(jiǎn)單的了解了加載流程,最后附上大閑魚(yú)的一張編譯大圖:

在這里插入圖片描述

混合開(kāi)發(fā)

所以我覺(jué)得Flutter應(yīng)該跟ReactNative類(lèi)似只要把相關(guān)的bundle文件放入我們app的assets即可砍鸠,所以拿這個(gè)方向開(kāi)始編譯Flutter代碼厘肮,開(kāi)開(kāi)心心的輸入flutter run之后在AS中怎么就是找不到相關(guān)產(chǎn)物,作為Android開(kāi)發(fā)者知道肯定會(huì)有個(gè)build目錄怎么就是不顯示睦番。所以去電腦對(duì)應(yīng)的盤(pán)中看了下是有這么個(gè)build目錄但是AS不顯示类茂,這樣子辦事很慢所以這里需要先加一個(gè)gradle task

task flutterPlugin << {
    println "工程目錄 = ${project.rootDir}/"
    println "編譯成功的位置 = ${this.buildDir}/"

    def projectName = this.buildDir.getPath()
    projectName = projectName.substring(0, projectName.length() - "app/".length())

    def rDir = new File("${this.rootDir}/FlutterPlugin/")
    def bDir = new File(projectName)

    if (!rDir.exists()) {
        rDir.mkdirs()
    } else {
        rDir.deleteDir()
    }

    bDir.eachDir {File dir ->
        def subDir = dir.getPath()
        def flutterJarDirName = subDir.replace("${projectName}/", "")
        def flutterJarDir = null
        if (subDir.contains("app")) {// 如果是app目錄的話 拷貝編譯后生成的flutter目錄
            flutterJarDir = new File("${subDir}/intermediates/assets/")
        } else {
            flutterJarDir = new File("${subDir}/intermediates/intermediate-jars/")
        }
        project.copy {
            from flutterJarDir
            into "${rDir}/${flutterJarDirName}"
        }
    }
}

把看不到的build中產(chǎn)物給拷貝出來(lái),將結(jié)果放入工程的FlutterPlugin目錄下:

在這里插入圖片描述

紅色框內(nèi)的東西是Flutter的gradle插件產(chǎn)生的依賴(lài)包托嚣,我們也是需要的巩检,所以順便一起拷貝出來(lái),那需要在哪示启?看下面的這個(gè)類(lèi)就知道了兢哭。

public final class GeneratedPluginRegistrant {
  public static void registerWith(PluginRegistry registry) {
    PathProviderPlugin.registerWith(registry.registrarFor("io.flutter.plugins.pathprovider.PathProviderPlugin"));
    SharedPreferencesPlugin.registerWith(registry.registrarFor("io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin"));
  }
}

到此為止我們把編譯flutter的產(chǎn)物都拷貝出來(lái),所以我們直接將這些產(chǎn)物放入我們的遠(yuǎn)程工程對(duì)應(yīng)的assets以及l(fā)ib路徑中去夫嗓〕俾荩可是對(duì)應(yīng)的FlutterActivity還是報(bào)紅,所以說(shuō)flutter還有一些產(chǎn)物沒(méi)有被我們發(fā)現(xiàn)舍咖。這時(shí)也不知道是什么玩意矩父,所以就找大閑魚(yú)的文章<貼在末尾>,最終找到了還有一個(gè)flutter.jar包沒(méi)有引入排霉。

在這里插入圖片描述

這就是最終在原生的工程下新建了一個(gè)fluttermodule模塊的最終層級(jí)關(guān)系了窍株。然后把demo中的類(lèi)相關(guān)拿進(jìn)來(lái)通過(guò)startActivity成功的進(jìn)入到FlutterActivity。
這里還是要把大閑魚(yú)說(shuō)的相關(guān)產(chǎn)物解釋附上:
在這里插入圖片描述

混合開(kāi)發(fā)的巨坑:
很開(kāi)心的運(yùn)行然后用AS打開(kāi)一看對(duì)應(yīng)的flutter.so確是armv8a的框架,如果說(shuō)直接拿到我們app中去就掛了因?yàn)槲覀僡pp中:

ndk {
     abiFilters "armeabi-v7a"
}

因?yàn)槲覀冎挥?code>v7a的框架球订,這就很頭痛了后裸。

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

我們的新建flutter項(xiàng)目有這么一個(gè)gradle文件,所以說(shuō)so兼容問(wèn)題肯定是這貨引起的冒滩。所以跟著進(jìn)去看看哪里有貓膩....
還算比較順利很快找到原因 原來(lái)這個(gè)gradle插件會(huì)自動(dòng)的幫你找到最適合當(dāng)前環(huán)境的so文件微驶,所以我們只需要強(qiáng)制讓它返回v7a的即可:

Path baseEnginePath = Paths.get(flutterRoot.absolutePath, "bin", "cache", "artifacts", "engine")
            String targetArch = 'arm'
            // if (project.hasProperty('target-platform') &&
            //     project.property('target-platform') == 'android-arm64') {
            //   targetArch = 'arm64'
            // }
            // targetArch = 'arm'

也就是說(shuō)讓targetArch為arm即可,所以說(shuō)flutter混合進(jìn)來(lái)的時(shí)候最大的坑就是我覺(jué)得就是so兼容問(wèn)題开睡,索性還是比較順利因苹。

Flutter動(dòng)態(tài)更新方案

當(dāng)我完成混合成功之后,我就在想能不能像其他的混合開(kāi)發(fā)庫(kù)能實(shí)現(xiàn)動(dòng)態(tài)更新士八。這里再次感謝大閑魚(yú)的思路:因?yàn)榇箝e魚(yú)說(shuō)直接把data/data/xxxxx下的vm_snapshot_data、vm_snapshot_instr梁呈、isolate_snapshot_data婚度、isolate_snapshot_instr替換成新編譯成功的那么界面加載出來(lái)的就是新的界面,所以說(shuō)這不就是動(dòng)態(tài)更新嗎官卡?
所以說(shuō)跟著節(jié)奏試試蝗茁,將編譯出來(lái)的打包成zip放入sd卡中去...
第一步:

  /**
  * 解壓SD路徑下的flutter包
  */
 public static void doUnzipFlutterAssets() throws Exception {
     String sdCardPath = Environment.getExternalStorageDirectory().getPath() + File.separator;
     String zipPath = sdCardPath + "flutter_assets.zip";
     File zipFile = new File(zipPath);
     if (zipFile.exists()) {
         ZipFile zFile = new ZipFile(zipFile);
         Enumeration zList = zFile.entries();
         ZipEntry zipEntry;
         byte[] buffer = new byte[1024];

         while (zList.hasMoreElements()) {
             zipEntry = (ZipEntry) zList.nextElement();
             Log.w("Jacyuhou", "==== zipEntry Name = " + zipEntry.getName());
             if (zipEntry.isDirectory()) {
                 String destPath = sdCardPath + zipEntry.getName();
                 Log.w("Jayuchou", "==== destPath = " + destPath);
                 File dir = new File(destPath);
                 dir.mkdirs();
                 continue;
             }

             OutputStream out = new BufferedOutputStream(new FileOutputStream(new File(sdCardPath + zipEntry.getName())));
             InputStream is = new BufferedInputStream(zFile.getInputStream(zipEntry));

             int len;
             while ((len = is.read(buffer)) != -1) {
                 out.write(buffer, 0, len);
             }
             out.flush();
             out.close();
             is.close();
         }
         zFile.close();
     }
 }

第二步:

    /**
     * 拷貝到data/data路徑下
     */
    public static void doCopyToDataFlutterAssets(Context mContext) throws Exception {
        String destPath = PathUtils.getDataDirectory(mContext.getApplicationContext()) + File.separator + "flutter_assets/";
        String originalPath = Environment.getExternalStorageDirectory().getPath() + File.separator + "flutter_assets/";
        Log.w("Jayuchou", "===== dataPath = " + destPath);
        Log.w("Jayuchou", "===== originalPath = " + originalPath);
        File destFile = new File(destPath);
        File originalFile = new File(originalPath);

        File[] files = originalFile.listFiles();
        for (File file : files) {
            Log.w("Jayuchou", "===== file = " + file.getPath());
            Log.w("Jayuchou", "===== file = " + file.getName());
            if (file.getPath().contains("isolate_snapshot_data")
                    || file.getPath().contains("isolate_snapshot_instr")
                    || file.getPath().contains("vm_snapshot_data")
                    || file.getPath().contains("vm_snapshot_instr")) {
                doCopyToDestByFile(file.getName(), originalFile, destFile);
            }
        }
    }

將對(duì)應(yīng)的文件拷貝到data目錄下去,跑起來(lái)看看 總算是成功了...

在這里插入圖片描述

看上面的gif圖寻咒,一開(kāi)的Flutter界面上顯示null 那么你完了線上的包顯示null錯(cuò)誤哮翘,所以這時(shí)就需要緊急發(fā)個(gè)補(bǔ)丁包,然后經(jīng)過(guò)Http下載下來(lái)重新打開(kāi)界面就修復(fù)了這個(gè)錯(cuò)誤饭寺。
所以說(shuō)這就是動(dòng)態(tài)更新的方案...

END...

感謝大閑魚(yú)的優(yōu)秀文章給的思路:
https://zhuanlan.zhihu.com/p/40528502
https://yq.aliyun.com/articles/607014

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末叫挟,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子抹恳,更是在濱河造成了極大的恐慌员凝,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件健霹,死亡現(xiàn)場(chǎng)離奇詭異瓶蚂,居然都是意外死亡糖埋,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén)窃这,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)阶捆,“玉大人,你說(shuō)我怎么就攤上這事倍奢±萜澹” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵畔裕,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我扮饶,道長(zhǎng)乍构,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任岂丘,我火速辦了婚禮眠饮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘仪召。我一直安慰自己,他們只是感情好钥庇,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布咖摹。 她就那樣靜靜地躺著,像睡著了一般萤晴。 火紅的嫁衣襯著肌膚如雪吐句。 梳的紋絲不亂的頭發(fā)上店读,一...
    開(kāi)封第一講書(shū)人閱讀 49,185評(píng)論 1 284
  • 那天屯断,我揣著相機(jī)與錄音侣诺,去河邊找鬼氧秘。 笑死,一個(gè)胖子當(dāng)著我的面吹牛丸相,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播膳算,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼弛作,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了机隙?” 一聲冷哼從身側(cè)響起刊头,我...
    開(kāi)封第一講書(shū)人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤诸尽,失蹤者是張志新(化名)和其女友劉穎您机,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體际看,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年脑溢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了赖欣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片顶吮。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖悴了,靈堂內(nèi)的尸體忽然破棺而出违寿,到底是詐尸還是另有隱情熟空,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布菌瘪,位于F島的核電站阱当,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏录淡。R本人自食惡果不足惜油坝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望彬檀。 院中可真熱鬧瞬女,春花似錦、人聲如沸诽偷。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)眠冈。三九已至,卻和暖如春布卡,著一層夾襖步出監(jiān)牢的瞬間诫舅,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工这弧, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人皇帮。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓蛋辈,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親渐白。 傳聞我的和親對(duì)象是個(gè)殘疾皇子逞频,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容