Chimera Flutter Code Push動態化熱更新介紹

Chimera發表於2021-03-16

Chimera Flutter Code Push動態化熱更新介紹

一、前言

在如今這個程式語言百花齊放、百家爭鳴的年代,誰能佔領移動端App開發的市場,誰就會受到各大廠商的青睞;而在移動端開發領域,如果誰能在跨平臺技術上有所突破,那她便更成為百家語言中的花中之魁。眾所周知,Google主導開發的Flutter就以其“多端一致”和“渲染效能”上的優勢讓其他跨端方案望塵莫及。

Flutter支援現有所知的所有平臺(iOS,android,Windows,Linux, Web..),並且支援一套程式碼全平臺一鍵部署。

然而,出於對效能和安全的考量,Google於2019 年 4 月主動放棄了對於Flutter動態化熱更新的支援,這便成了Flutter這門“超級框架”的美中不足。為了滿足市場的需求和技術的突破,我們維彤科技Chimera團隊從2019年底開始對Flutter動態化更新進行立項研究,截止今年12月初,我們的動態化熱更新SDK和對接方案已告一段落,產品代號也為“Chimera”。

二、什麼是Chimera?

Chimera(奇美拉)是傳說中希臘的一隻神獸,他是一個“混合體”, 他擁有獅子的頭顱,山羊的身軀和蟒蛇的尾巴,並擁有超強的戰鬥力。

我們的產品就像是奇美拉一樣,可以為各個平臺提供一套完善的整體嵌入式解決方案,我們的SDK具有易接入,易整合,易編譯的優勢和特點,不管你的app是在開發初期,還是app已經開發完了,都是一鍵無縫接入,同時整個程式碼整合和編譯過程都可謂是“傻瓜式”介入。我們的產品提供細粒度熱更新能力,無需等待發版即可實現修復應用線上問題,終端使用者全程無感。

2.1 產品優勢

  • 通用性,即時一鍵接入現有Flutter專案,不會對源App開發程式有任何影響
  • 低成本,動態化對齊 Flutter 生態和常規開發習慣,且可低成本轉化現有的 Flutter 頁面
  • 適用性,修復範圍廣,編譯包體積小,無侵入,無第三方語言介入高效能,保持Flutter App自身效能,無任何效能影響損耗
  • 好服務,一站式整體體驗,支援雙端商業化平臺,擁有高可複用的開發體驗
我們的Github開源地址: github.com/Waytoon/chi…

2.2 實現具體過程:

Chimera Flutter熱更新(CodePush)是一個SDK,已經開發了2年以上,用於Flutter應用程式、程式碼熱更新的增強和完善。

如您所知,Google Flutter團隊已宣佈,Flutter將不支援其引擎中的程式碼推送功能。

Google在這裡提到:

This was previously on our roadmap for 2019. After investigating this in greater detail, we have decided not to proceed with this work for now.

There were several factors that led us to this decision:

  • To comply with our understanding of store policies on Android and iOS, any solution would be limited to JIT code on Android and interpreted code on iOS. We are not confident that the performance characteristics of such a solution on iOS would reach the quality that we demand of our product. (In other words, "it would be too slow".)
  • There are some serious security concerns. Since these patches would essentially allow arbitrary code execution, they would be extremely attractive malware vectors. We could mitigate this by requiring that patches be signed using the same key as the original package, but this is error prone and any mistake would have serious consequences. This is, fundamentally, the same problem that has plagued platforms that allow execution of code from third-party sources. This problem could be mitigated by integrating with a platform update mechanism, but this defeats the purpose of an out-of-band patching mechanism.
  • There is currently no out-of-the-box open source hosting solution for patching applications, so we would either have to rely on people configuring their Web servers accordingly, or we would have to create integrations for proprietary third-party services, or we would have to create our own bespoke solution. Hosting patches is a space we are not eager to enter. Having people configure their own server leaves them open to making mistakes with potentially serious implications as explained in the previous point about security. Depending on third-party services puts Flutter in an awkward position of having to pick winners and exposes us to the risk of those projects themselves making policy changes that would affect this feature.

因此,Chimera Flutter CodePush在那時就已經進行了初始化和開發:Chimera CodePush是一種雲服務,使Flutter開發人員可以將移動應用程式更新直接部署到其使用者的裝置上。它充當中央儲存庫的角色,開發人員可以將更新發布到該儲存庫,並且應用程式可以從中查詢更新。在解決錯誤和/或新增不需要重新構建二進位制檔案並通過各自的應用商店重新分發二進位制檔案的小功能時,這使您可以與使用者群建立更具確定性和直接的參與度模型。

Chimera可以將捆綁包中的UI / Logic / ..(所有您想要的)更改分發給使用者,方法類似於其他開源熱更新Project,但是又有所不同。通過整合Flutter Chimera,您可以釋出所有應用程式頁面,而無需等待應用程式的下一個釋出日期。 Chimera提供了一個出色的編譯器,它可以用作轉換,執行,升級Flutter頁面/應用程式全新魔術工具。我們的SDK基於Dart原生語言,沒有使用任何第三方語言做任何開發,所以使用我們的SDK對於flutter開發者非常友好,對您來說,沒有任何的學習成本,可以說是一鍵秒上手!

要開始使用Chimera CodePush,請參考本文件,如果您想從原始碼構建/貢獻專案,請閱讀以下步驟。

Chimera SDK將支援所有以flutter編寫的應用程式,請毫不猶豫地開始使用此超級程式碼推送SDK。 Dev Setup

2.3 DEV Setup

現在開始:

? 首先你可能需要先成為一個出色的Flutter程式設計師或者是一面flutter程式設計愛好者,像我一樣:-),主要是需要熟悉Flutter程式設計,還有一顆喜歡鑽研和學習的心。

設定Flutter程式碼環境:here.

你需要一個用的得手的 IDE, 推薦使用Android Studio 或 VSCODE. 注意:在git克隆此倉庫之前,最好安裝“ Git Large File Storage”(git-lfs.github.com/)。 它可以幫助您處理和同步大檔案,以避免任何資料流丟失。

切記要安裝 http-server,因為後續的工作需要http-server:
brew install http-server
複製程式碼

大殺器在這裡,我們的編譯器!這個編譯器便是主要的工具來服務Chimera CodePush。

# 編譯工具下載連結
下載 link:
https://github.com/Waytoon/chimera_flutter_code_push/releases/download/v1.0.0/rust_compile

複製程式碼

對於中國使用者,您可以參考以下說明連結以加快GitHub檔案的下載速度。

Speedy access github

以下環境當前SDK不支援::

  1. Windows

重要的事情:編譯器執行時需要聯網。全面支援Mac,包括M1.

? 請檢查您計算機上的執行時環境。 Chimera基於Flutter最新的穩定版本。並且SDK將來會繼續支援最新的穩定Flutter版本。

flutter.dev/docs/develo…

我們使用的flutter版本(stable版本)

 ~/Desktop/tool  flutter --version                      
Flutter 1.22.6 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 9b2d32b605 (6 weeks ago) • 2021-01-22 14:36:39 -0800
Engine • revision 2f0af37152
Tools • Dart 2.10.5
複製程式碼

如果你現在使用的是flutter 2,請先切換到flutter 1.22.6 stale

切換方法 請在終端執行 flutter downgrade

2.4 使用 CodePush SDK

  1. 首先執行mkdir test建立一個test資料夾。如下圖:

    iShot2021-03-07 10.15.19

    cd test進入test目錄建立一個hello專案:flutter create hello

    iShot2021-03-07 10.16.14

    hello目錄結構如下:

    .hello folder
    ├── README.md
    ├── android
    ├── hello.iml
    ├── ios
    ├── lib
    ├── pubspec.lock
    ├── pubspec.yaml
    └── test
    複製程式碼

2.用IDE開啟hello這個專案:

iShot2021-03-07 10.26.37

這裡一定要注意!! 使用IDE除錯一次hello這個專案,不管是使用模擬器還是真機,以下我們使用模擬器作為示例演示。

image-20210307103440696

  1. 很重要的一步下載我們的編譯器,連結

    https://github.com/Waytoon/chimera_flutter_code_push/releases/download/v1.0.0/rust_compile

請注意檢視並關注release版塊的release檔案的釋出,最近一段時間,我們會每晚更新一次編譯器。

下載到一個指定目錄,隨意給一個名字,我們就用tool吧:

iShot2021-03-07 10.57.58

然後開啟終端terminal,進入編譯器的目錄,執行命令chmod 777 rust_compile,以使得編譯器有許可權可以執行

下一步在終端中執行./rust_compile

image-20210307110040282

第一次執行編譯器,會生成配置檔案config.yaml

  1. 執行後會提示配置config.yaml,如上圖所示。

    開啟config.yaml並配置如下內容:

  # Project path
  projectPath: /Users/ryanliu/Desktop/test/hello #修改成hello的目錄
  # Flutter SDK
  flutterSdkPath: /Users/ryanliu/development/flutter #修改當前flutter環境的SDK目錄
  # Compile cache path
  buildCachePath: /Users/ryanliu/Desktop/temp/dart_compile_cache #設定一個專門目錄儲存dart_compile_cache
  # FTP
  #ftpConfig: 192.168.80.141|root|123456  此處可忽略
複製程式碼
  1. 然後在終端中再次執行./rust_compile執行我們的編譯過程。

    大概2-5分鐘的樣子,就可以得到下面的畫面結果,編譯的產物hello.bin在專案hello/assets/目錄下,同時多出了wtbase庫目錄。

    image-20210307110402262

    我們可以看到hello資料夾目錄變化為,注意目錄裡多出了兩個新的資料夾assetswtbase

    ├── README.md
    ├── android
    ├── assets
    ├── build
    ├── hello.iml
    ├── ios
    ├── lib
    ├── pubspec.lock
    ├── pubspec.yaml
    ├── test
    └── wtbase
    複製程式碼
  2. 下面這一步非常重要! 配置wtbase/pubspec.yamldependencies 修改flutter_code_push的引導路徑為以下:

         flutter_code_push:
             git:
               url: https://github.com/Waytoon/chimera_flutter_code_push.git
               path: flutter_code_push
複製程式碼

原配置:

image-20210307113315222

修改之後: image-20210307113436135

記得點一下上面的pub get,或者在terminal裡輸入flutter pub get.

image-20210307113615328

  1. 然後修改hello/pubspec.yaml,在dependencies新增如下:

wtbase: path: ./wtbase

並將dependencies下的其他依賴庫剪下複製到wtbase/pubspec.yaml’的dependencies`下,最終效果如下:

hello/pubspec.yaml:

image-20210307151156273

hello/wtbase/pubspec.yaml:

image-20210307151326749

記得執行flutter pub get,或者點選Pub get按鈕。

注意事項: 請先執行wtbase目錄的Pub get, 然後在執行hello專案的Pub get。這樣做的目的是為了保證專案依賴庫的版本號保持一致性。
截止到這裡,初步的SDK配置工作已經完成,下面我們講解如何在我們的source code裡使用和實現熱更新。
  1. 現在在hello/lib目錄下建立MyApp.dart檔案,Source code如下:
import 'package:flutter/material.dart';
            import 'package:dio/dio.dart';
            
            void initApp() {
              runApp(MyApp());
            }
            
            class MyApp extends StatelessWidget {
              // This widget is the root of your application.
              @override
              Widget build(BuildContext context) {
                return MaterialApp(
                  title: 'Flutter Demo',
                  theme: ThemeData(
                    // This is the theme of your application.
                    //
                    // Try running your application with "flutter run". You'll see the
                    // application has a blue toolbar. Then, without quitting the app, try
                    // changing the primarySwatch below to Colors.green and then invoke
                    // "hot reload" (press "r" in the console where you ran "flutter run",
                    // or simply save your changes to "hot reload" in a Flutter IDE).
                    // Notice that the counter didn't reset back to zero; the application
                    // is not restarted.
                    primarySwatch: Colors.blue,
                    // This makes the visual density adapt to the platform that you run
                    // the app on. For desktop platforms, the controls will be smaller and
                    // closer together (more dense) than on mobile platforms.
                    visualDensity: VisualDensity.adaptivePlatformDensity,
                  ),
                  home: MyHomePage(title: 'Flutter Demo Home Page'),
                );
              }
            }
            
            class MyHomePage extends StatefulWidget {
              MyHomePage({Key key, this.title}) : super(key: key);
            
              // This widget is the home page of your application. It is stateful, meaning
              // that it has a State object (defined below) that contains fields that affect
              // how it looks.
            
              // This class is the configuration for the state. It holds the values (in this
              // case the title) provided by the parent (in this case the App widget) and
              // used by the build method of the State. Fields in a Widget subclass are
              // always marked "final".
            
              final String title;
            
              @override
              _MyHomePageState createState() => _MyHomePageState();
            }
            
            class _MyHomePageState extends State<MyHomePage> {
              int _counter = 0;
              String _response;
            
              void _incrementCounter() {
                setState(() {
                  // This call to setState tells the Flutter framework that something has
                  // changed in this State, which causes it to rerun the build method below
                  // so that the display can reflect the updated values. If we changed
                  // _counter without calling setState(), then the build method would not be
                  // called again, and so nothing would appear to happen.
                  _counter++;
                });
              }
            
              void _getHttp() async {
                try {
                  Response response = await Dio().get("http://www.google.com");
                  setState(() {
                    String temp = '$response';
                    for (var i = 0; i < 10; ++i) {
                      temp = '$i' + temp;
                    }
                    _response = temp.substring(0, 100);
                  });
                } catch (e) {
                  print(e);
                }
              }
            
            
              @override
              void initState() {
                _getHttp();
                super.initState();
              }
            
              @override
              Widget build(BuildContext context) {
                // This method is rerun every time setState is called, for instance as done
                // by the _incrementCounter method above.
                //
                // The Flutter framework has been optimized to make rerunning build methods
                // fast, so that you can just rebuild anything that needs updating rather
                // than having to individually change instances of widgets.
                return Scaffold(
                  appBar: AppBar(
                    // Here we take the value from the MyHomePage object that was created by
                    // the App.build method, and use it to set our appbar title.
                    title: Text(widget.title),
                  ),
                  body: Center(
                    // Center is a layout widget. It takes a single child and positions it
                    // in the middle of the parent.
                    child: Column(
                      // Column is also a layout widget. It takes a list of children and
                      // arranges them vertically. By default, it sizes itself to fit its
                      // children horizontally, and tries to be as tall as its parent.
                      //
                      // Invoke "debug painting" (press "p" in the console, choose the
                      // "Toggle Debug Paint" action from the Flutter Inspector in Android
                      // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
                      // to see the wireframe for each widget.
                      //
                      // Column has various properties to control how it sizes itself and
                      // how it positions its children. Here we use mainAxisAlignment to
                      // center the children vertically; the main axis here is the vertical
                      // axis because Columns are vertical (the cross axis would be
                      // horizontal).
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: <Widget>[
                        Text(
                          'You have pushed the button this many times:',
                        ),
                        Text(
                          '$_counter',
                          style: Theme.of(context).textTheme.headline4,
                        ),
                        Text(
                       'Server Response:\n$_response'
                        )
                   ],
                    ),
               ),
                  floatingActionButton: FloatingActionButton(
                    onPressed: _incrementCounter,
                    tooltip: 'Increment',
                    child: Icon(Icons.add),
                  ), // This trailing comma makes auto-formatting nicer for build methods.
                );
              }
            }
複製程式碼

新增完這個檔案後,記得執行flutter pub get一下,確保依賴正確匯入。

然後我們修改main.dart檔案如下:

        ```dart
        import 'package:hello/MyApp.dart';
        import 'package:wtbase/wtbase.dart';
        import 'package:flutter/material.dart';
        
        void main() async {
          WidgetsFlutterBinding.ensureInitialized();
        
          bool isNative = false;
          if (isNative) {
            initApp();
          } else {
            String downloadUrl = "http://yourlocalhost/hello.bin";
            readCode = WTAnalysisReadCode();
            await readCode.loadFile(downloadUrl);
            readCode.executeMethod(
                'package:hello/MyApp.dart', 'initApp');
          }
        }
        ```
複製程式碼

請注意downloadUrl現在僅支援內網測試。downloadUrl需要以http://192.168開頭。

如需支援外網請求,請聯絡flupus@gmail.com

另一個至關重要的步驟

開啟終端terminal,進入hello/assets目錄: image-20210307141550767

然後在terminal輸入:http-server -c -1

結果如下:

image-20210307142308107

複製http-server下面的連結,我的是http://192.168.80.115:8080,用來修改main.dart裡的

            String downloadUrl = "http://yourlocalhost/hello.bin";
複製程式碼

修改成:

            String downloadUrl = "http://192.168.80.115:8080/hello.bin";
複製程式碼

到此為止整個的接入過程已經告一段落了。

  1. 下面的事情就是從IDE執行一下你的APP,方法就是點選那個綠蟲子。

image-20210307142840043

不出任何意外,IDE的編譯不會出現任何問題。模擬器上的顯示是這樣的。

image-20210307144439925

DE的debug console log如下:

            Launching lib/main.dart on Android SDK built for x86 in debug mode...
            Running Gradle task 'assembleDebug'...
            ✓ Built build/app/outputs/flutter-apk/app-debug.apk.
            Installing build/app/outputs/flutter-apk/app.apk...
            Waiting for Android SDK built for x86 to report its views...
            Debug service listening on ws://127.0.0.1:65046/r1MGSW-wdg4=/ws
            Syncing files to device Android SDK built for x86...
            W/m.example.hell( 6898): Accessing hidden method Lsun/misc/Unsafe;->getUnsafe()Lsun/misc/Unsafe; (greylist,core-platform-api, linking, allowed)
            W/m.example.hell( 6898): Accessing hidden method Lsun/misc/Unsafe;->objectFieldOffset(Ljava/lang/reflect/Field;)J (greylist,core-platform-api, linking, allowed)
            W/m.example.hell( 6898): Accessing hidden method Lsun/misc/Unsafe;->compareAndSwapObject(Ljava/lang/Object;JLjava/lang/Object;Ljava/lang/Object;)Z (greylist, linking, allowed)
            W/m.example.hell( 6898): Accessing hidden method Lsun/misc/Unsafe;->putObject(Ljava/lang/Object;JLjava/lang/Object;)V (greylist, linking, allowed)
            D/EGL_emulation( 6898): eglMakeCurrent: 0xd7a8e900: ver 3 0 (tinfo 0xdaf0f820)
            D/eglCodecCommon( 6898): setVertexArrayObject: set vao to 0 (0) 1 0
            I/flutter ( 6898): download file!
            I/flutter ( 6898): vmClassName: MyApp className: WTStatelessWidget1
            I/flutter ( 6898): vmClassName: MyHomePage className: WTStatefulWidget1
            I/flutter ( 6898): vmClassName: _MyHomePageState className: WTState1
複製程式碼
  1. 後面的事情就是任由你自己發揮了。比如我們修改MyApp.dart裡的程式碼試一下。修改floatingActionButton的icon試一下吧:
            floatingActionButton: FloatingActionButton(
                    onPressed: _incrementCounter,
                    tooltip: 'Increment',
                    child: Icon(Icons.add),
                  ),
複製程式碼

Icons.add修改為Icons.home。完成後是這樣的:

            floatingActionButton: FloatingActionButton(
                    onPressed: _incrementCounter,
                    tooltip: 'Increment',
                    child: Icon(Icons.home),
                  ),
複製程式碼

儲存你所做的修改,然後回到terminal,進入你的編譯器目錄,執行./rust_compile

image-20210307143903868

執行這個命令後,等待一段時間,最終效果如下:

image-20210307144050787

這裡親請注意了,我們到此不需要IDE來做什麼了,只需要在模擬器裡關掉當前執行的app,重新開啟它,你就會發現它變成了下面的樣子:

image-20210307143122217

再次強調一下到此為止我們上面的這一步操作沒有使用IDE的任何功能,我們只是用我們自己的編譯器編譯了新的hello.bin檔案,然後通過模擬器遠端載入了這個bin檔案。

###這就是一個可以完整熱更新的APP了。

到此為止,你已經開發了一個帶有熱更新功能的App了,是不是很神奇?! 你現在可以隨意去改一些程式碼試試,改過你的程式碼後記得要再次編譯哦(重複第10步)。

喜歡的話就給我們點個星星吧✨!

##注意事項:

第一: 對於android app釋出:

對於安卓App開發,需要提前給App賦予網路和儲存許可權,這點很重要。修改hello/app/src/main/AndroidManifest.xml

image-20210307135636779

第二: 對於iOS app釋出:

對於iOS App開發:需要在 "Architectures" 設定中新增, "arm64 & x86_64" 。否則你編譯的時候會報出錯誤。

更改步驟如下:

最初預設是這樣的:

image-20210307140112482

修改過程:首先點other

image-20210307140300631

會出現以下視窗:

image-20210307140445851

然後點+ 出現以下:

image-20210307140532395

在文字框裡輸出arm64。 再次點+,新增x86_64;會有兩項被新增。

image-20210307140746620

最後選中$(ARCHS_STANDARD),點選上圖中的-,刪除$(ARCHS_STANDARD)

最終效果:

image-20210307140930332

注意,如果iOS編譯的時候,如果XCODE報x86_64.a檔案相關的編譯錯誤的時候,請將Architecturesx86_64刪除掉,再次執行XCODE釋出。刪除x86_64將不支援iOS模擬器了。

提醒,如果你想部署APP到iPhone真機並不用連線iPhone資料線到電腦的話,請Profile你的APP到手機上,XCODE快捷鍵是

⌘ + i

對於Android apk釋出,可能會遇到的問題:

在釋出android apk的時候,如果你使用flutter build apk 的時候會報錯如下:

This application cannot tree shake icons fonts. It has non-constant instances of IconData at the following locations:
  - file:///Users/ryanliu/Desktop/test/hello/wtbase/lib/generate/mapping/WTVMIconData.dart:31:12
複製程式碼

請使用如下執行命令釋出apk:

flutter build apk --no-tree-shake-icons
複製程式碼

你就不會再遇見上面那個錯誤了。

相關的介紹資料

更多的資訊我們隨後會放在一下連結:

?️ 我們 2021 的計劃

  • 春天·開源在 2021的高鐵上
  • 下一代的神奇 V2 in 2021
    • 實現差異化更新
    • 全面AOT支援
    • AI 智慧編譯器

⚠️不支援的內容??

  • 現在不謙虛的說我們支援了所有的Flutter語法和Pub庫.
  • 可能會有一些意外的bug,我們也不想,那就請大家一起去把它們殺光吧,德瑪西亞

?如果你喜歡可以加入我們!

我們在此熱烈歡迎來自五湖四海的兄弟姐妹加入我們,我們需要社群裡大家的支援和幫忙,真誠歡迎大家來到我們的github主頁:github.com/Waytoon/chi… 歡迎大家點贊轉發評論。再次感謝!

歡迎掃描下面的碼子加入我們:

  • 可以新增SDK群主WX: 搜尋 novolei

相關文章