[Flutter]漫读Flutter的Gradle构建脚本

1,944 阅读7分钟

漫读flutter.gradle脚本。

项目中,通过implementation project(':flutter')的方式,在主工程的build.gradle目录下,以模块的方式引入了一个flutter工程。

而在flutter工程的build.gradle中,直接通过apply from引入了flutter.gradle脚本:

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

因此,一般你可以在这个目录找到它:

image.png

flutterRoot对应的就是flutter SDK的目录,而在Android项目中,集成、编译Flutter工程,自然而然地离不开flutter.gradle的参与。

我们可以在上述的路径中,找到flutter.gradle文件,并拖到idea或者Android Studio中打开。

一、目录

整个脚本文件的全貌还是比较简单的:

一共七个部分,乍一看我们不难看出:

  1. FlutterExtensions是一个配置类,一些省略的数据可以更加灵活地去设置;
  2. buildScript、android闭包就更不用说了,几乎每个Android开发都知道它们的含义;
  3. Apply plugin,显然就是应用了下面的FlutterPlugin;
  4. FlutterPlugin显然就是我们Flutter嵌入Android工程(Android的Gradle工程)的核心;
  5. BaseFlutterTask,显然就是一个Flutter的GradleTask的抽象表达。
  6. 而FlutterTask就是一个对应的实现,其中实现了类似于获取输入/输出目录、获取assets资源目录等等功能的具体实现。

显然,FlutterPlugin就是flutter.gradle运行的核心。

二、FlutterPlugin

有尝试过学习Gradle的同学一定对这串代码不陌生:

class FlutterPlugin implements Plugin<Project>{//...}

这是用来声明一个自定义Gradle插件的Gradle类,因为它类似Java的语法使得本身能够被Java/Kotlin开发者所读懂,一个完整的Gradle插件的定义应该如下:


class MyPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        // ....
    }
}

我们需要对传入的apply对象进行配置,自然我们会想要去阅读apply相关的方法,其实不多,大概就283-121,大约162行。

三、addFlutterDeps

3.1 isFlutterAppProject

apply方法一开始就通过isFlutterAppProject方法对当前的项目文件结构进行判断,如果当前的Gradle Properties中,含有applicationVariants参数,那么就会去执行项目根目录下的gradlew程序:

def gradlew = (OperatingSystem.current().isWindows()) ?
        "$ { rootProject.projectDir } /gradlew.bat" : "$ { rootProject.projectDir } /gradlew"
rootProject.exec {
workingDir rootProject.projectDir
    executable gradlew
    args ":$ { subproject.name } :dependencies", "--write-locks"
} 

workingDir指定了工作目录,类似于Shell的cd操作,切换到项目的根目录,然后执行Windows下的批处理程序gradlew.bat或者是其它平台的gradlew程序,然后再其后拼接了args参数。

至此我们其实已经有所收获了,我们知道如何在Gradle中,通过指定工作目录、程序和参数去执行一个程序了,def gradlew本身是在声明一个路径,一个脚本文件的路径,然后.exec函数可以帮助我们去执行一些东西。

3.2 FLUTTER_STORAGE_BASE_URL与镜像仓库

在flutter官方文档中,通常会建议我们将Flutter的镜像地址,替换为国内的镜像地址:

在这里我通过在系统的环境变量里面设置了一个FLUTTER_STORAGE_BASE_URL变量,在flutter.gradle脚本中,会通过System.env.FLUTTER_STORAGE_BASE_URL把这个环境变量读出来,然后通过为rootProject统一配置一个基于该域名的仓库。

3.3 FlutterExtensions

接下来的一段代码是:

project.extensions.create("flutter", FlutterExtension)

即向工程注册了一个FlutterExtensions扩展,并命名为flutter,这里的FlutterExtension,其实就是flutter.gradle开头声明的第一份部分,包括如下的内容:

class FlutterExtension {
    static int compileSdkVersion = 31

    static int minSdkVersion = 16

    static int targetSdkVersion = 31

    String source

    String target
}

3.4 Flutter相关的Task1:compileTask

我们知道,Gradle通过构建Task与Task间的先后关系,生成一个有向无环图(DAG)来完成有序的构建,如果项目嵌入了Flutter,那么Flutter的构建本身,也会作为Android工程构建的一个或者多个步骤,在众多task之间,按既定顺序排列,显然,接下来就是整个FlutterGradle构建的关键。

接下来的100行左右都是在从project.property中取一些配置值,例如:

String[] fileSystemRootsValue = null
if (project.hasProperty('filesystem-roots')) {
    fileSystemRootsValue = project.property( 'filesystem-roots' ).split( '\|' )
}
String fileSystemSchemeValue = null
if (project.hasProperty('filesystem-scheme')) {
    fileSystemSchemeValue = project.property('filesystem-scheme')
}
……

这里获取到的一些数值,例如fileSystemRootsValuefileSystemSchemeValue等等,会统一在后续使用。

接下来,会通过如下的代码完成对Task的创建:

String taskName = toCammelCase(["compile", FLUTTER_BUILD_PREFIX, variant.name])
FlutterTask compileTask = project.tasks.create(name: taskName, type: FlutterTask){
    ...
}

仅通过阅读我们可能很难看出taskName最终的取值是什么,这个时候println大法就派上用场了:

println "(from_source_code)-> $taskName"

我们在运行自己的Flutter项目之后,这里就会输出:

(from_source_code)-> compileFlutterBuildDebug

这里的compile和FLUTTER_BUILD_PREFIX(常量:flutterBuild)的值都是固定的,而最后的debug,则取自你的variant.name,即Android工程配置的变体的名称,如果你在这里选择构建的是debug模式的包,那么就是debug;如果选择构建release模式,这里就会是complileFlutterBuildRelease

project.tasks.create的一次完整调用其实后面还跟着一个闭包,大概30多行:

FlutterTask compileTask = project.tasks.create(name: taskName, type: FlutterTask) {
 flutterRoot this.flutterRoot  //   / Users / yzyi / sdk / flutter
 flutterExecutable this.flutterExecutable  //   / Users / yzyi / sdk / flutter / bin / flutter
buildMode variantBuildMode // debug
    localEngine this.localEngine // null
    localEngineSrcPath this.localEngineSrcPath // null
    targetPath getFlutterTarget() // lib/main.dart
    verbose isVerbose() // false
    fastStart isFastStart() // false
    fileSystemRoots fileSystemRootsValue // null
    fileSystemScheme fileSystemSchemeValue // null
    trackWidgetCreation trackWidgetCreationValue // true
    targetPlatformValues = targetPlatforms // [android - arm, android - arm64, android - x64]
    sourceDir getFlutterSourceDirectory() //Users/yzyi/workdir/{your_project_path}
    intermediateDir project.file("$ { project.buildDir } /$ { AndroidProject.FD_INTERMEDIATES } /flutter/$ { variant.name } /")
     //Users/yzyi/workdir/{your_project_path}/.android/Flutter/build/intermediates/flutter/debug/
    extraFrontEndOptions extraFrontEndOptionsValue // null 
    extraGenSnapshotOptions extraGenSnapshotOptionsValue // null
    splitDebugInfo splitDebugInfoValue // null
    treeShakeIcons treeShakeIconsOptionsValue // false
    dartObfuscation dartObfuscationValue // false
    dartDefines dartDefinesValue // null
    bundleSkSLPath bundleSkSLPathValue // null
    performanceMeasurementFile performanceMeasurementFileValue // null
    codeSizeDirectory codeSizeDirectoryValue // null
    deferredComponents deferredComponentsValue // false
    validateDeferredComponents validateDeferredComponentsValue // true
    doLast {
project.exec {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
                commandLine('cmd', '/c', "attrib -r $ { assetsDirectory } /* /s")
            } else {
                commandLine('chmod', '-R', 'u+w', assetsDirectory)
            }
        }
}
} 

执行一次debug构建的参数大致上已经写上去了,其实就是对这一次的任务创建的配置构建。

最后,在doLast闭包中,向构建的尾部加入了一个commandLine语句,chmod本意是在修改一个文件的访问权限:chmod -R u+w该指令表示-R:递归地向下,u+w表示给用户加上写入的权限,而对应的目录,就是assetsDirectory,它是一个在FlutterTask中定义的内容,而它的构成,则又是通过上面提到的**intermediateDir/flutter_assets**构成的:

在doLast阶段为flutter_assets的输出目录为当前用户加上写权限。

3.5 Flutter相关的Task2:packFlutterAppAotTask

到这一步flutter.gradle会再创建一个新的Task,首先会找到一个Jar包:

File libJar = project.file("$ { project.buildDir } /$ { AndroidProject.FD_INTERMEDIATES } /flutter/$ { variant.name } /libs.jar")

它的文件路径是:

/Users/yzyi/workdir/{your_project_path}/.android/Flutter/build/intermediates/flutter/debug/libs.jar

我们可以猜测,这部分就是Flutter中Java部分的代码,随后创建了一个新的task:

Task packFlutterAppAotTask = project.tasks.create(name: "packLibs$ { FLUTTER_BUILD_PREFIX } $ { variant.name.capitalize() } ", type: Jar) {
destinationDir libJar.parentFile
   archiveName libJar.name
  dependsOn compileTask
    targetPlatforms.each { targetPlatform ->
String abi = PLATFORM_ARCH_MAP[targetPlatform]
        from("$ { compileTask.intermediateDir } /$ { abi } ") {
include "*.so"
            // Move `app.so` to `lib/<abi>/libapp.so`
            rename { String filename ->
return "lib/$ { abi } /lib$ { filename } "
            }
}
}
} 

首先,我们可以看到它的顺序是依赖于complieTask的,其次,这是一个jar类型的task,结合它的名字:packFlutterAppAotTask,我们可以猜它的作用就是把指定的内容打入对应的jar包,而destinationDir则指定了jar文件的地址,archiveName则指定了jar产物的名称。

而最后一个targetPlatforms.each所做的事情,就是遍历所有的目标平台,以上面的内容为例,我们可以看到目标平台的取值如下:

targetPlatformValues = // [android - arm, android - arm64, android - x64] 

首先会从PLATFORM_ARCH_MAP这个map中拿到对应平台的abi名称,然后再将对应平台的地址:intermediateDir/${abi}`取出所有*.so文件,复制到目的地址:lib/abi/lib{abi}/lib{fileName}"。正如注释中所说的那样:

// Move `app.so` to `lib/<abi>/libapp.so`

就是一个将app.so复制到目的文件地址的过程。

但是,这个任务并不是和compileTask本身一样,它并不是任何时候都触发的,我们知道,debug模式下的libapp.so其实并不存在,转而被一种特殊的二进制文件所替代(为了能够支持JIT实现快速的热重载),正如这个Task名字描述的那样:packFlutterAppAotTask,这其实是一个用于移动位于临时地址的对应/libapp.so到目的lib//libapp.so地址的任务。

如果你的Flutter工程以Release、Profile模式执行,那么你的Flutter代码就会以libapp.so二进制的程序存在,这里会对对应的libapp.so进行拷贝。

> Task :flutter:packLibsflutterBuildProfile
copy:/Users/yzyi/{your_project_dir}/flutter_module/.android/Flutter/build/intermediates/flutter/profile/armeabi-v7a/app.so to lib/armeabi-v7a/libapp.so

> Task :flutter:packLibsflutterBuildProfile
copy:/Users/yzyi/{your_project_dir}/flutter_module/.android/Flutter/build/intermediates/flutter/profile/arm64-v8a/app.so to lib/arm64-v8a/libapp.so

> Task :flutter:packLibsflutterBuildProfile
copy:/Users/yzyi/{your_project_dir}/flutter_module/.android/Flutter/build/intermediates/flutter/profile/x86_64/app.so to lib/x86_64/libapp.so

我们可以看到,这个方法将三个对应平台、ABI的libapp.so从flutter_module中的临时文件夹(intermediates)复制到Jar包中,最终他们都会进入到我们之前提到的libs.jar当中:

最后这个Jar包在Gradle中会被我们的主工程所引用,这些so文件也会被添加到我们最终的apk当中。

但是,即使debug模式下也会对对应的libs进行打包,但是debug模式下Flutter并不依赖于libapp.so进行运行,因此,我们看看打出来的libs.jar中含有什么内容:

也没啥有用的内容,那么我们的Flutter代码呢?我们之前在juejin.cn/post/718953… "kernel_blob.bin" 的形式存在,这也是一个二进制文件。

其实平时我们对这一类非常规文件,我们一般都把他扔在assets里面。比如一些存储在本地的.json、字体.ttf等等,我们直接看intermediates/flutter/debug/flutter_assets中的文件,就能找到这个kernel_blob.bin

相比profile版本的flutter_assets,debug版本的flutter_assets文件夹中多了这些内容:

-rw-r--r--@ 1 yzyi staff 4.8M 3 24 2022 isolate_snapshot_data

-rw-r--r--@ 1 yzyi staff 66M 4 24 14:18 kernel_blob.bin

-rw-r--r--@ 1 yzyi staff 11K 3 24 2022 vm_snapshot_data

hot reload的实现逻辑,就是将新编译出来的二进制文件推送到设备上以实现快速的热重载,显然,如果我们手动去替换assets的内容也能实现手动的替换。

3.6 构建FlutterPlugin插件(构建AAR)

boolean isBuildingAar = project.hasProperty('is-plugin')

首先,properties中的属性:is-plugin用来标记是否将本Flutter工程构建为一个AAR包,给其他Android 工程引入,由于这里是直接引入了一个Flutter工程,这里的Flutter_module就不会去打成AAR包了,因此isBuildingAar的取值会是false。

接下来声明了两个task:

  • packageAssets
  • cleanPackageAssets
boolean isUsedAsSubproject = packageAssets && cleanPackageAssets && !isBuildingAar

isUsedAsSubproject这个变量主要用于甄别当前的Flutter工程,是否被用作一个子工程。

我们知道,一般来说Android工程会有一个主工程,并且它的工程名称一定是:app。这样一来:app就会是主工程,而:flutter则会变成子工程。

原文注释是:

We know that `:flutter` is used as a subproject when these tasks exists and we aren't building an AAR.

大致上就是说,当我们在构建AAR并且:app:flutter同时存在的时候,:flutter目录才会是子工程。

根据这个点,我们创建了第三个Task:

Task copyFlutterAssetsTask = project.tasks.create(
        name: "copyFlutterAssets$ { variant.name.capitalize() } ",
        type: Copy,
){
    // ......
} 

它的类型是Copy,名字也是Copy,就是一个搬运文件的Task,在debug模式下,它的名称是:copyFlutterAssetsDebug,省略号构成的Closure中,同样对闭包进行了配置:

{
    dependsOn compileTask
    with compileTask.assets
    if (isUsedAsSubproject) {
        dependsOn packageAssets
        dependsOn cleanPackageAssets
        into packageAssets.outputDir
        return
    }
    // `variant.mergeAssets` will be removed at the end of 2019.
    def mergeAssets = variant.hasProperty("mergeAssetsProvider") ?
            variant.mergeAssetsProvider.get() : variant.mergeAssets
    dependsOn mergeAssets
    dependsOn "clean$ { mergeAssets.name.capitalize() } "
    mergeAssets.mustRunAfter("clean$ { mergeAssets.name.capitalize() } ")
    into mergeAssets.outputDir
}

首先会依赖于compileTask任务,也就是Flutter核心的构建任务。

其次会连接到compileTask的assets目录:

@Internal
CopySpec getAssets() {
    return project.copySpec {
from "$ { intermediateDir } "
        include  "flutter_assets/**"  // the working dir and its files
    }
}

接下来会根据是否子工程构建(isUsedAsSubProject变量)走两条不同的构建路径:

  • 子工程构建:
if (isUsedAsSubproject) {
    dependsOn packageAssets
    dependsOn cleanPackageAssets
    into packageAssets.outputDir
 return
}

比较明显就是依赖于上面提到的两个子工程构建相关的任务,完成后输出到AAR包对应的outputDir目录中。

  • 非子工程构建:
// `variant.mergeAssets` will be removed at the end of 2019.
def mergeAssets = variant.hasProperty("mergeAssetsProvider") ?
        variant.mergeAssetsProvider.get() : variant.mergeAssets
dependsOn mergeAssets
dependsOn "clean$ { mergeAssets.name.capitalize() } "
mergeAssets.mustRunAfter("clean$ { mergeAssets.name.capitalize() } ")
into mergeAssets.outputDir

和子工程构建比较相似,区别就在于构建的产物最终会写到mergeAsset相关的目录中。

至此copyFlutterAssetsTask就算完成了,其实就是一个Copy Flutter Asset的过程。

接下来的代码会从variant的输出中拿到processResourcesProvider,并将它依赖到copyFlutterAssetsTask上。

至此本文最大的一个部分:addFlutterDeps的分析就完成了。但是addFlutterDeps本身也只是一个Closure,它会在未来的某个时间去执行,上述的内容都只是定义罢了:

...上文
def addFlutterDeps = { variant ->
 /// 上面的分析
}  //  end def addFlutterDeps
下文...

其实关于compileTask章节还有一个很重要的内容没有提到,compileTask是如何完成app.so文件的构建的?

四、FlutterApk的构建相关

最终将compileTask添加到构建的代码其实直到这里才开始。

Apk的构建,同样划分了两种情况,如果是满足isFlutterAppProject的项目,最后会走一套Flutter为主工程的流程(单独工程);而其余情况则会走Flutter作为子工程的构建流程,需要和主工程(:app)做交互(作为子工程进行嵌入)。

4.1 主工程

首先会通过project.android.applicationVariants.all去遍历当前Flutter主工程中的所有Application变体。拿到变体之后,会作为参数,输入到addFlutterDeps当中去:

Task copyFlutterAssetsTask = addFlutterDeps(variant)
def variantOutput = variant.outputs.first()
def processResources = variantOutput.hasProperty("processResourcesProvider") ?
        variantOutput.processResourcesProvider.get() : variantOutput.processResources
processResources.dependsOn(copyFlutterAssetsTask)

然后将资源处理程序依赖到copyFlutterAssetsTask任务执行完成之后。

最终,再通过variant.outputs.all去遍历该变体下的所有输入文件,在对应的assembleTask的末尾插入一个Closure,然后就是一个根据ABI、变体等等对APK文件进行重命名的逻辑,完成之后将文件拷贝到outputs目录。

4.2 嵌入工程

嵌入工程中,会先尝试通过findProject方法去拿到主工程的Gradle Project对象:

String hostAppProjectName = project.rootProject.hasProperty('flutter.hostAppProjectName') ? project.rootProject.property('flutter.hostAppProjectName') : "app"
Project appProject = project.rootProject.findProject(":$ { hostAppProjectName } ")

然后在appProject的afterEvaluate阶段做如下的操作:

project.android.libraryVariants.all { libraryVariant ->
Task copyFlutterAssetsTask
    appProject.android.applicationVariants.all { appProjectVariant ->
Task appAssembleTask = getAssembleTask(appProjectVariant)
        if (!shouldConfigureFlutterTask(appAssembleTask)) {
            return
        }
        String variantBuildMode = buildModeFor(libraryVariant.buildType)
        if (buildModeFor(appProjectVariant.buildType) != variantBuildMode) {
            return
        }
        if (copyFlutterAssetsTask == null) {
            copyFlutterAssetsTask = addFlutterDeps(libraryVariant)
        }
        Task mergeAssets = project
                .tasks
                .findByPath(":$ { hostAppProjectName } :merge$ { appProjectVariant.name.capitalize() } Assets")
        assert mergeAssets
        mergeAssets.dependsOn(copyFlutterAssetsTask)
    }
} 

这里和构建主工程的代码逻辑上还是比较相似的,步骤也是优先获取assembleTask、执行addFlutterDeps方法最后通过mergeAssets完成资源合并。

4.3 嵌入式Flutter项目和主工程构建模式分离

如何让Android的主工程以Debug模式运行,而内嵌的Flutter工程以Release/Profile模式运行?

官方的计数器demo可以使用flutter run --release指令执行run一个release模式的包,但是我们在Android主工程中以Gradle的形式,直接引入一个flutter项目工程,我们似乎没有直接指定--release的地方。

其实观察compleTask提供的参数,我们不难发现,它的第三行就指定了构建模式为:variantBuildMode,而variantBuildMode又是根据variant.buildType得到的:

private static String buildModeFor(buildType) {
    if (buildType.name == "profile") {
        return "profile"
    } else if (buildType.debuggable) {
        return "debug"
    }
    return "release"
}

其实关键之处,就在于variant.buildType,如果它的name == "profile"那么最终的产物就会以profile模式执行;如果它是可以debug的(debuggable的),那么最终的产物就会是debug,其余场景都会是release模式。

为了速度的话,可以直接注释掉其他行,只保留最后一行,这样打包出来的产物就可以是单独地以Release模式运行的Flutter模块了。

正确的做法,自然是去修改variant.buildType的值。我们知道,Variant,变体是在主工程的build.gradle中通过android闭包声明、指定的:

android{
    buildTypes {
        debug {
            debuggable true
        }
        release {
            debuggable false            
        }
    ......
}

这里的debug和release,其实就是我们主工程的两个变体,name就是他们的名称本身:debug和release,我们并没有一个名为profile的变体名称,因为一般我们的需求就只需要两个变体版本debug和release。

因此,结合**buildModeFor** 的实现,我们其实选择不多,如果我们想要主工程debuggable,那么我们就不能修改debuggable的值, 显然主工程也会依赖这个值来判断是否加入debug信息,代码中的原文注释如下:

// This mapping is based on the following rules:

// 1. If the host app build variant name is profile then the equivalent

// Flutter variant is profile.

// 2. If the host app build variant is debuggable

// (e.g. buildType.debuggable = true), then the equivalent Flutter

// variant is debug.

// 3. Otherwise, the equivalent Flutter variant is release.

因此,我们只能新增一个variant:profile,它的配置和debug一模一样:

android{
    buildTypes {
        profile {
            debuggable true
        }
        debug {
            debuggable true
        }
        release {
            debuggable false            
        }
    ......
}

但是由于**buildModeFor** 会优先判断名称,profile 这个变体能够忽略debuggable属性的判断,从而达到主工程是debuggable的,而flutter工程是采用AOT编译的profile模式。

五、compileTask与FlutterTask

我们前面虽然已经介绍了compileTask,但是你会发现一个事情,我们从compileTask几乎是毫无征兆地就跳到了libs.jar的包装过程Task:packFlutterAppAotTask,而完全没有提到libs.jar中的app.so是如何构建的,这个其实已经被定义在FlutterTask当中了,这也是本文的最后一个部分:BaseFlutterTask与FlutterTask。

BaseFlutterTask已经定义了很多的属性内容,在FlutterTask实现它之后就可以对这些属性内容进行重写以实现多态,但是真正和构建相关的,其实是buildBundle方法,整体内容不多,也就70多行,逻辑也比较简单,大致如下:

  1. 创建intermediateDir文件夹;
  2. project.exec去执行构建脚本;

这里的关键点其实在project.exec,我们之前在调用Gradlew的时候已经走过这个方法了,核心就三个点:

  1. 工作目录:workingDir
  2. 可执行文件/脚本文件:executable
  3. 参数:arg

它基本上等价于我们直接在shell中做如下操作:

cd $workingDir
$executable $arg // 用可执行完成执行程序,以args为参数

flutter的构建使用的就是flutter.bat或者flutter脚本文件,不难想想它的参数格式就是:

cd flutter_sdk_dir
flutter $args

我们只需要将参数打印出来,我们就可以自己完成flutter源码->app.so或者kernel_blob.bin的构建过程:

println "(from_source_code)-> plzRunExecute:$ { flutterExecutable.absolutePath } $ { args } ,$ { ruleNames.toString() } in $ { sourceDir } "

然后我们打开一个终端,然后手动CD到sourceDir的目录,然后执行上述的指令:

/Users/yzyi/sdk/flutter/bin/flutter\
  assemble\
  --no-version-check\
    --quiet\
  --depfile /Users/yzyi/{your_flutter_module_dir}/.android/Flutter/build/intermediates/flutter/release/flutter_build.d\
  --output /Users/yzyi/{your_flutter_module_dir}/.android/Flutter/build/intermediates/flutter/release\
  -dTargetFile=lib/main.dart\
  -dTargetPlatform=android\
  -dBuildMode=release\
  -dTrackWidgetCreation=true\
  android_aot_bundle_release_android-arm android_aot_bundle_release_android-arm64 android_aot_bundle_release_android-x64

其中的参数:android_aot_bundle_release_android-arm android_aot_bundle_release_android-arm64 android_aot_bundle_release_android-x64**部分其实是ruleNames的参数,其余的参数绝大多数都是在对assemble进行配置。

经过这个步骤之后,你就可以拿到在packFlutterAppAotTask中被用来出来的app.so了,也就是我们程序对应的libapp.so