前面一篇文章我們探討了一下Flutter App如何被整合到iOS App中的,本文我們接著來討論下Flutter App如何被整合到Android App中的。
Gradle
我們簡單看一下Android專案的程式碼結構:
作為Android專案的自動化構建工具,我們先來看看Gradle在Flutter APP的構建過程中大概做了哪些工作。
settings.gradle
在settings.gradle中主要是用來配置Android Project中所有需要依賴的module,即進行工程樹的配置。
settings.gradle
// 1
include ':app'
// 2
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
// 3
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
複製程式碼
- 首先引入app module
- 讀取local.properties這個檔案中的
flutter.sdk
屬性的值,賦值給flutterSdkPath
這個變數
- local.properties中除了Android SDK路徑,還定義了Flutter相關的一些值。如Flutter SDK路徑,Flutter構建模式,Flutter版本編號等。
local.properties
sdk.dir=/Users/*/Library/Android/sdk
flutter.sdk=/Users/*/Documents/flutter
flutter.buildMode=debug
flutter.versionName=1.0.0
flutter.versionCode=1
複製程式碼
- 引入
"$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
中的指令碼
我們接下來來看看app_plugin_loader.gradle指令碼的程式碼:
app_plugin_loader.gradle
import groovy.json.JsonSlurper
def flutterProjectRoot = rootProject.projectDir.parentFile
// 1 找到配置檔案
def pluginsFile = new File(flutterProjectRoot, '.flutter-plugins-dependencies')
if (!pluginsFile.exists()) {
return
}
// 2 讀取配置檔案
def object = new JsonSlurper().parseText(pluginsFile.text)
assert object instanceof Map
assert object.plugins instanceof Map
assert object.plugins.android instanceof List
object.plugins.android.each { androidPlugin ->
assert androidPlugin.name instanceof String
assert androidPlugin.path instanceof String
def pluginDirectory = new File(androidPlugin.path, 'android')
assert pluginDirectory.exists()
include ":${androidPlugin.name}"
project(":${androidPlugin.name}").projectDir = pluginDirectory
}
複製程式碼
- 讀取Android檔案同級目錄下的
.flutter-plugins-dependencies
檔案 - 讀取該檔案下的plugins欄位下的android陣列,對陣列的每個元素配置依賴。
提示: 是不是很熟悉? 沒錯iOS的Pod指令碼讀的就是這個檔案plugins欄位下的的ios欄位的值。
{
"plugins":{
"android":[
...
{
"name":"sqflite",
"path":"/Users/*/Documents/flutter/.pub-cache/hosted/pub.dartlang.org/sqflite-1.3.2+3/",
"dependencies":[
]
}
...
],
"ios":[...],
}
}
複製程式碼
總結一下,最後settings.gradle大致生成的內容如下所示:
include ':app'
include ':fijkplayer'
project(":fijkplayer").projectDir = '/Users/*/Documents/flutter/.pub-cache/hosted/pub.dartlang.org/fijkplayer-0.8.7/android'
include ':shared_preferences'
project(":shared_preferences").projectDir = '/Users/*/Documents/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.5.12+4/android'
include ':sqflite'
project(":sqflite").projectDir = '/Users/*/Documents/flutter/.pub-cache/hosted/pub.dartlang.org/sqflite-1.3.2+3/android'
include ':url_launcher'
project(":url_launcher").projectDir = '/Users/*/Documents/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-5.7.10/android'
複製程式碼
總結:settings.gradle中完成了所有依賴的module的配置。
Project / build.gradle
我們來看看Project / build.gradle中的一些設定:
// 1.
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
// 2.
subprojects {
project.evaluationDependsOn(':app')
}
複製程式碼
- 設定project和子project的輸出路徑,路徑為和android同級的build資料夾下。
進去瞅瞅就可以驗證:
你估計發現了,iOS的編譯結果也是放在這個目錄裡面的。
- 通過
evaluationDependsOn
定義了所有其他的moudule的配置都依賴於app這個moudule。即其他的所有moudule配置必須得等app這個moudule的配置完成後再進行配置。
總結:Project / build.gradle中配置了各個moudule的編譯輸出路徑和moudule間的依賴關係。
app / build.gradle
接下來我們來看下app / build.gradle中的內容:
// 1.
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
// 2.
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
// 3.
flutter {
source '../..'
}
複製程式碼
程式碼解釋:
- 第一大段的作用是從local.properties檔案中讀取版本編號和版本名稱,設定為Android App的版本編號和版本名稱;
- 根據從local.properties檔案中讀取到的
$flutterRoot
路徑匯入$flutterRoot/packages/flutter_tools/gradle/flutter.gradle
的指令碼執行; - 給flutter擴充套件的
source
屬性配置為.. / ..
。
flutter.gradle
flutter.gradle
的目的是在Android 宿主App的編譯構建流程中執行一些Flutter相關的任務。
flutter.gradle中有兩個重要的類,一個是FlutterPlugin,一個是FlutterTask。
FlutterPlugin
FlutterPlugin作為Gradle Plugin實現了Plugin介面,所以它的入口方法是apply()
方法:
FlutterPlugin/apply
void apply(Project project) {
...
// 1.
project.extensions.create("flutter", FlutterExtension)
// 2
project.afterEvaluate this.&addFlutterTasks
// 3
if (shouldSplitPerAbi()) {
project.android {
splits {
abi {
enable true
reset()
universalApk false
}
}
}
}
getTargetPlatforms().each { targetArch ->
String abiValue = PLATFORM_ARCH_MAP[targetArch]
project.android {
if (shouldSplitPerAbi()) {
splits {
abi {
include abiValue
}
}
}
}
}
// 4
String flutterRootPath = resolveProperty("flutter.sdk", System.env.FLUTTER_ROOT)
if (flutterRootPath == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file or with a FLUTTER_ROOT environment variable.")
}
flutterRoot = project.file(flutterRootPath)
if (!flutterRoot.isDirectory()) {
throw new GradleException("flutter.sdk must point to the Flutter SDK directory")
}
engineVersion = useLocalEngine()
? "+" // Match any version since there's only one.
: "1.0.0-" + Paths.get(flutterRoot.absolutePath, "bin", "internal", "engine.version").toFile().text.trim()
String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter"
flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile();
// 5
project.android.buildTypes {
profile {
initWith debug
if (it.hasProperty("matchingFallbacks")) {
matchingFallbacks = ["debug", "release"]
}
}
}
// 6
if (shouldShrinkResources(project)) {
String flutterProguardRules = Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools",
"gradle", "flutter_proguard_rules.pro")
project.android.buildTypes {
release {
shrinkResources isBuiltAsApp(project)
proguardFiles project.android.getDefaultProguardFile("proguard-android.txt"), flutterProguardRules, "proguard-rules.pro"
}
}
}
// 7
if (useLocalEngine()) {
String engineOutPath = project.property('local-engine-out')
File engineOut = project.file(engineOutPath)
if (!engineOut.isDirectory()) {
throw new GradleException('local-engine-out must point to a local engine build')
}
localEngine = engineOut.name
localEngineSrcPath = engineOut.parentFile.parent
}
// 8
project.android.buildTypes.each this.&addFlutterDependencies
project.android.buildTypes.whenObjectAdded this.&addFlutterDependencies
}
複製程式碼
程式碼解釋:
- 建立一個FlutterExtension擴充套件,這個擴充套件有兩個屬性,
source
-Flutter APP工程的路徑,target
- Flutter APP的執行入口,不設定就預設lib/main.dart; - app module的其他Task完成後執行
addFlutterTasks
方法; - 決定是否開啟abi分包;
- 獲取一些系統環境變數;
- flutterRootPath --- /Users/*/Documents/flutter
- flutterRoot --- /Users/*/Documents/flutter
- engineVersion --- 1.0.0-2c956a31c0a3d350827aee6c56bb63337c5b4e6e
- flutterExecutable --- flutter(mac), flutter.bat(windows)
- 預設有debug和release兩個模式,這裡參照debug模式又新增了一個profile構建模式,所以現在變成了debug,release和profile三個構建模式;
- 是否啟動shrinkResources資源縮減;
- 是否設定本地maven倉庫;
- 給每個構建模式新增Flutter依賴
addFlutterDependencies
的呼叫。
接下來我們看看addFlutterDependencies
中的實現:
FlutterPlugin/addFlutterDependencies
void addFlutterDependencies(buildType) {
...
// 1.
String hostedRepository = System.env.FLUTTER_STORAGE_BASE_URL ?: DEFAULT_MAVEN_HOST
String repository = useLocalEngine()
? project.property('local-engine-repo')
: "$hostedRepository/download.flutter.io"
project.rootProject.allprojects {
repositories {
maven {
url repository
}
}
}
// 2
addApiDependencies(project, buildType.name,
"io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion")
print("io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion\n");
// 3
List<String> platforms = getTargetPlatforms().collect()
if (flutterBuildMode == "debug" && !useLocalEngine()) {
platforms.add("android-x86")
platforms.add("android-x64")
}
platforms.each { platform ->
String arch = PLATFORM_ARCH_MAP[platform].replace("-", "_")
addApiDependencies(project, buildType.name,
"io.flutter:${arch}_$flutterBuildMode:$engineVersion")
print("io.flutter:${arch}_$flutterBuildMode:$engineVersion\n");
}
}
複製程式碼
程式碼解釋:
- 設定maven倉庫的url地址,預設是storage.googleapis.com/download.fl…, 如果網速不太理想也可以配置FLUTTER_STORAGE_BASE_URL環境變數,讓其指向國內的映象地址**storage.flutter-io.cn/download.fl…
- 新增嵌入式的依賴
io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion
, 這個依賴與構建模式和Flutter Engine版本有關係。例子-io.flutter:flutter_embedding_debug:1.0.0-2c956a31c0a3d350827aee6c56bb63337c5b4e6e
- 新增libflutter.so依賴,這個依賴和架構和Flutter Engine版本有關係。例子-
io.flutter:armeabi_v7a_debug:1.0.0-2c956a31c0a3d350827aee6c56bb63337c5b4e6e
提示:
- flutter_embedding的作用是賦予Flutter嵌入native的能力;
- libflutter.so就是Flutter Engine;
- 這兩個依賴最後呼叫的是
project.dependencies.add(configuration, dependency, config)
這個方法,所以是給project加的依賴。因為sqflite等其他的module都需要這兩個依賴。
至此FlutterPlugin/apply的流程已經分析完了,接下來我們就來分析第2步遺留的addFlutterTasks
方法。
FlutterPlugin/addFlutterTasks
這個方法的程式碼量比較大,我們來概括總結一下。
private void addFlutterTasks(Project project) {
// 1
String target = project.flutter.target
if (target == null) {
target = 'lib/main.dart'
}
// 2
def addFlutterDeps = { variant ->
2.1
FlutterTask compileTask = project.tasks.create(name: taskName, type: FlutterTask) { ... }
2.2
Task packFlutterAppAotTask = project.tasks.create(name: "packLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", type: Jar) { ... }
2.3
addApiDependencies(project, variant.name, project.files {
packFlutterAppAotTask
})
2.4
Task copyFlutterAssetsTask = project.tasks.create(name: "copyFlutterAssets${variant.name.capitalize()}",
type: Copy,
) { ... }
}
// 3
project.android.applicationVariants.all { variant -> ...}
// 4
configurePlugins()
}
複製程式碼
- 從gradle.properties獲取各種引數,上面給出的一個例子---如果沒有target配置,就預設設定為lib/main.dart。
- 定義一個addFlutterDeps的函式.
- 根據構建模式和第一步從gradle.properties獲取各種引數一起建立對應的FlutterTask。FlutterTask的功能則是編譯Flutter APP的程式碼。
- packFlutterAppAotTask這個task是將FlutterTask的編譯結果打包成libs.jar檔案。
- 給project加上libs.jar的檔案依賴。
- copyFlutterAssetsTask是進行Flutter App相關的asset進行拷貝。由於Flutter可能作為外掛編譯或子專案編譯,如果是外掛編譯產物編譯結果打包為AAR,子專案編譯則子專案編譯時,編譯結果被打包成APK,所以兩種情況下有區別處理。
- 為所有applicationVariants或libraryVariants新增Flutter依賴,執行addFlutterDeps函式然後把APK拷貝到目標路徑。
- 這個方法是給project新增Plugin的依賴.編譯方式不同依賴的處理方式也不一樣。
FlutterTask
FlutterTask的build()
呼叫的是父類的buildBundle()
方法:
void buildBundle() {
if (!sourceDir.isDirectory()) {
throw new GradleException("Invalid Flutter source directory: ${sourceDir}")
}
intermediateDir.mkdirs()
// Compute the rule name for flutter assemble. To speed up builds that contain
// multiple ABIs, the target name is used to communicate which ones are required
// rather than the TargetPlatform. This allows multiple builds to share the same
// cache.
String[] ruleNames;
if (buildMode == "debug") {
if (fastStart) {
ruleNames = ["faststart_android_application"]
} else {
ruleNames = ["debug_android_application"]
}
} else {
ruleNames = targetPlatformValues.collect { "android_aot_bundle_${buildMode}_$it" }
}
project.exec {
logging.captureStandardError LogLevel.ERROR
executable flutterExecutable.absolutePath
workingDir sourceDir
if (localEngine != null) {
args "--local-engine", localEngine
args "--local-engine-src-path", localEngineSrcPath
}
if (verbose) {
args "--verbose"
} else {
args "--quiet"
}
args "assemble"
args "--depfile", "${intermediateDir}/flutter_build.d"
args "--output", "${intermediateDir}"
if (performanceMeasurementFile != null) {
args "--performance-measurement-file=${performanceMeasurementFile}"
}
if (!fastStart || buildMode != "debug") {
args "-dTargetFile=${targetPath}"
} else {
args "-dTargetFile=${Paths.get(flutterRoot.absolutePath, "examples", "splash", "lib", "main.dart")}"
}
args "-dTargetPlatform=android"
args "-dBuildMode=${buildMode}"
if (trackWidgetCreation != null) {
args "-dTrackWidgetCreation=${trackWidgetCreation}"
}
if (splitDebugInfo != null) {
args "-dSplitDebugInfo=${splitDebugInfo}"
}
if (treeShakeIcons == true) {
args "-dTreeShakeIcons=true"
}
if (dartObfuscation == true) {
args "-dDartObfuscation=true"
}
if (dartDefines != null) {
args "--DartDefines=${dartDefines}"
}
if (bundleSkSLPath != null) {
args "-iBundleSkSLPath=${bundleSkSLPath}"
}
if (codeSizeDirectory != null) {
args "-dCodeSizeDirectory=${codeSizeDirectory}"
}
if (extraGenSnapshotOptions != null) {
args "--ExtraGenSnapshotOptions=${extraGenSnapshotOptions}"
}
if (extraFrontEndOptions != null) {
args "--ExtraFrontEndOptions=${extraFrontEndOptions}"
}
args ruleNames
}
}
複製程式碼
這個方法其實是執行帶一些引數的flutter build方法。
APK
最後,我們來看看APK的結構。
- 在lib檔案中預設包含了4種架構的支援,裡面包括了Flutter Engine---
libflutter.so
,Flutter App程式碼---libapp.so
,Flutter外掛依賴的so檔案---libijkffmpeg.so,libijkplayer.so,libijksdl.so
;
你可能會遇到
couldn't find "libflutter.so"
,因為x86裡面沒有libflutter.so和libapp.so
,這是flutter的一個已知的問題。
- Flutter App中的資原始檔都打包到了
assets/flutter_assets
中。
AndroidManifest.xml
我們來看看AndroidManifest.xml
裡面都配置了些什麼東東:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.jj_movie">
// 1.
<application
android:name="io.flutter.app.FlutterApplication"
android:label="jj_movie"
android:icon="@mipmap/ic_launcher">
<activity
...>
// 2
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
// 3
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background"
/>
</activity>
// 4
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
複製程式碼
- 專案的Application的類為io.flutter.app.FlutterApplication;
- 預設定義了一個白色的主題,在Flutter APP載入顯示前使用者可見,在Flutter APP載入顯示後作為Window的背景;
- 可以修改啟動圖;
- 使用Flutter Android Embedding V2 版本。
FlutterActivity
,FlutterActivity
,FlutterActivity
和FlutterActivity
等類都是在V2版本中引入的。
FlutterApplication
FlutterApplication中的程式碼很簡單,就主要執行了FlutterInjector.instance().flutterLoader().startInitialization(this)
這行程式碼。
public class FlutterApplication extends Application {
@Override
@CallSuper
public void onCreate() {
super.onCreate();
FlutterInjector.instance().flutterLoader().startInitialization(this);
}
}
複製程式碼
FlutterLoader主要的作用就是載入FLutter Engine 和載入Flutter APP的資源等。我們看看FlutterLoader的startInitialization
方法中的主要程式碼:
public void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) {
// 1.
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new IllegalStateException("startInitialization must be called on the main thread");
}
// 2.
VsyncWaiter.getInstance((WindowManager) appContext.getSystemService(Context.WINDOW_SERVICE))
.init();
// 3.
Callable<InitResult> initTask =
new Callable<InitResult>() {
@Override
public InitResult call() {
...
}
};
}
複製程式碼
- 確保本方法是在主執行緒執行;
- VsyncWaiter進行初始化,它的主要作用是在Android系統註冊並等待VSync訊號。
VsyncWaiter是Flutter渲染的中繼者,當收到訊號後,會通知Flutter app發起渲染呼叫,然後執行一些列的layout和paint,最後提交給GPU執行緒合成上屏。
- 開啟一個非同步執行緒,載入一些asset資源。
總結:FlutterLoader執行startInitialization
是為Flutter app的 載入和渲染做好準備工作。
FlutterActivity
我們來看看FlutterActivity的重要程式碼:
public class FlutterActivity extends Activity {
// 1
protected FlutterActivityAndFragmentDelegate delegate;
// 2
private LifecycleRegistry lifecycle;
public FlutterActivity() {
lifecycle = new LifecycleRegistry(this);
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// 3.1
switchLaunchThemeForNormalTheme();
super.onCreate(savedInstanceState);
// 3.2
delegate = new FlutterActivityAndFragmentDelegate(this);
delegate.onAttach(this);
delegate.onActivityCreated(savedInstanceState);
// 3.3
configureWindowForTransparency();
// 3.4
setContentView(createFlutterView());
// 3.5
configureStatusBarForFullscreenFlutterExperience();
}
private View createFlutterView() {
return delegate.onCreateView(null , null, null);
}
}
複製程式碼
- FlutterActivityAndFragmentDelegate屬性
delegate
可以說是負責處理FLutter app相關的絕大部分功能的物件; - LifecycleRegistry是一個LifeCycle,處理FlutterActivity生命週期的事情,在建構函式中初始化;
- 在
onCreate
方法中主要的工作是:- 先切換到啟動圖的主題顯示啟動圖片
- 初始化FlutterActivityAndFragmentDelegate物件
delegate
- 然後將window的背景設定透明
- FlutterActivity的View新增由
delegate
物件建立的一個FlutterView,作為FLutter app的渲染View。FlutterView是SurfaceView的子類。 - Android 5.0以上設定為沉浸式狀態列
到此為止,一切都已經準備就緒了,就等FlutterActivityAndFragmentDelegate將內容載入進來了。
FlutterActivityAndFragmentDelegate
- 建構函式傳入FlutterActivity作為Host, 主要就是為了獲取context。
FlutterActivityAndFragmentDelegate(@NonNull Host host) {
this.host = host;
}
複製程式碼
onAttach
主要是初始化了Flutter Engine和註冊了Flutter外掛;
void onAttach(@NonNull Context context) {
ensureAlive();
if (flutterEngine == null) {
setupFlutterEngine();
}
host.configureFlutterEngine(flutterEngine);
}
複製程式碼
// FlutterActivity
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
GeneratedPluginRegister.registerGeneratedPlugins(flutterEngine);
}
複製程式碼
GeneratedPluginRegistrant這個註冊外掛的邏輯和iOS的類似。
- 載入Flutter App的內容
當FlutterActivity執行onStart
時會呼叫FlutterActivityAndFragmentDelegate的onStart
方法,然後就從lib/main.dart入口檔案開始執行了。
void onStart() {
doInitialFlutterViewRun();
}
private void doInitialFlutterViewRun() {
if (flutterEngine.getDartExecutor().isExecutingDart()) {
return;
}
if (host.getInitialRoute() != null) {
flutterEngine.getNavigationChannel().setInitialRoute(host.getInitialRoute());
}
String appBundlePathOverride = host.getAppBundlePath();
if (appBundlePathOverride == null || appBundlePathOverride.isEmpty()) {
appBundlePathOverride = FlutterInjector.instance().flutterLoader().findAppBundlePath();
}
DartExecutor.DartEntrypoint entrypoint =
new DartExecutor.DartEntrypoint(
appBundlePathOverride, host.getDartEntrypointFunctionName());
flutterEngine.getDartExecutor().executeDartEntrypoint(entrypoint);
}
複製程式碼