App Bundle构建可动态化下载模块的App

摘要

Android App Bundle 是一种发布格式,其中包含您应用的所有经过编译的代码和资源,它会将 APK 生成及签名交由 Google Play 来完成。

Google Play 会使用您的 App Bundle 针对每种设备配置生成并提供经过优化的 APK,因此只会下载特定设备所需的代码和资源来运行您的应用。您不必再构建、签署和管理多个 APK 来优化对不同设备的支持,而用户也可以获得更小且更优化的下载文件包。

对于大多数应用项目而言,构建 App Bundle 以支持提供经过优化的 APK 并不费力。例如,如果您已经按照既定惯例组织管理应用的代码和资源,只需使用 Android Studio 或使用命令行来构建已签名的 Android App Bundle,并将它们上传到 Google Play。然后,就能提供经过优化的 APK,自动获享其带来的优势了。

使用 App Bundle 格式发布应用时,您也可以选择使用 Play Feature Delivery,它可让您向应用项目中添加功能模块。这些模块包含仅根据您指定的条件随应用提供的功能和资源,或者稍后在运行时供使用 Play 核心库下载的功能和资源。

使用 App Bundle 发布应用的游戏开发者可以使用 Play Asset Delivery:它是 Google Play 用于分发大量游戏资产的解决方案,为开发者提供了灵活的分发方式和极高的性能。

在这里插入图片描述

创建module

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WOIzsNtC-1625621690603)(https://www.wolai.com/5pMZR2aQ8cHooXibELS7Q8#7aGH1EvLqW26aB31JSraJ5#pic_center)]
在这里插入图片描述
在这里插入图片描述

依赖

主模块App需要进行一下依赖:

implementation 'com.google.android.play:core-ktx:1.8.1'
implementation 'com.google.android.play:core:1.10.0'

Application

主模块App的Application需要继承SplitCompatApplication

class App : SplitCompatApplication()
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:dist="http://schemas.android.com/apk/distribution"
    package="cn.xxstudy.demo">

    <!--表示应用程序模块即时启用,-->
    <dist:module dist:instant="true" />

    <application
        android:name=".DemoApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.PlayCoreDemo">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Dynamic Feature Module

该模块功能代码层面我们就当一个普通module去处理就行,只是有几点不同:
- build.gradle

plugins {
    id 'com.android.dynamic-feature'
    ...
}
dependencies {
    //它是可以依赖app模块的,因此如果app模块的依赖方式是api,那么改模块也是可以共用其依赖的
    implementation project(":app")
    ...
}

- AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:dist="http://schemas.android.com/apk/distribution"
    package="cn.xxstudy.login_feature">

    <dist:module
        //指定是否应通过 Google Play 免安装体验为模块启用免安装体验。
        //在设置 <dist:on-demand/> 时,不能将此 XML 元素设置为 true
        dist:instant="false"
        dist:title="@string/title_login_feature">
        //封装自定义模块分发的选项,如下所示。
        //请注意,每个功能模块必须只配置这些自定义分发选项的一种类型。
        <dist:delivery>
            <dist:on-demand />
        </dist:delivery>
        //熔断性,如果是5.0一下的版本则打包为全量包
        <dist:fusing dist:include="true" />
    </dist:module>
    <application>
        //这是我们测试的登录Activity 注意Activity不能设置为exported:true 因为别的app不知道该模块是否已经下载
        <activity
            android:name=".LoginActivity"
            android:launchMode="singleTask" />
    </application>
</manifest>

功能模块清单

属性说明
<manifest ...这是您的典型 <manifest> 块。
xmlns:dist="http://schemas.android.com/apk/distribution"指定一个新的 dist: XML 命名空间,如下所述。
split="split_name"当 Android Studio 构建 app bundle 时,会包含该属性。因此,您不应自行添加或修改此属性
定义模块的名称,当应用使用 Google Play 核心库发出按需模块请求时会指定该名称。
Gradle 如何确定该属性的值
默认情况下,当您使用 Android Studio 创建功能模块时,IDE 会使用您指定的模块名称,在 Gradle 设置文件中将该模块标识为 Gradle 子项目。
当您构建 app bundle 时,Gradle 会使用子项目路径的最后一个元素将此清单属性注入模块的清单。例如,如果您在 MyAppProject/features/ 目录中创建了一个新功能模块,并指定了“dynamic_feature1”作为其模块名称,IDE 会在 settings.gradle 文件中添加 ':features:dynamic_feature1' 作为子项目。构建 app bundle 时,Gradle 会将 <manifest split="dynamic_feature1"> 注入模块的清单。
android:isFeatureSplit="true | false">当 Android Studio 构建 app bundle 时,会包含该属性。因此,您不应手动添加或修改此属性
指定此模块为功能模块。基本模块和配置 APK 中的清单要么省略此属性,要么将其设置为 false
<dist:module这一新的 XML 元素定义了一些属性,这些属性可确定如何打包模块并作为 APK 分发。
dist:instant="true | false"指定是否应通过 Google Play 免安装体验为模块启用免安装体验。
如果应用包含一个或多个启用免安装体验的功能模块,您也必须为基本模块启用免安装体验。如果您使用的是 Android Studio 3.5 或更高版本,当您创建支持免安装体验的功能模块时,IDE 会为您完成此操作。
在设置 <dist:on-demand/> 时,不能将此 XML 元素设置为 true。不过,您仍可使用 Play Core 库请求以免安装体验的形式按需下载支持免安装体验的功能模块。当用户下载并安装您的应用时,设备会默认下载并安装应用的支持免安装体验的功能模块以及基本 APK。
dist:title="@string/feature_name"为模块指定一个面向用户的名称。例如,当设备请求确认下载时,便可能会显示该名称。
您需要将此名称的字符串资源包含在基本模块的 module_root/src/source_set/res/values/strings.xml 文件中。
<dist:fusing dist:include="true | false" /> </dist:module>指定是否在面向搭载 Android 4.4(API 级别 20)及更低版本的设备的 multi-APK 中包含此模块。
此外,当您使用 bundletool 从 app bundle 生成 APK 时,只有将此属性设置为 true 的功能模块才会包含在通用 APK 中。通用 APK 是一个单体式 APK,其中包含了应用所支持的所有设备配置的代码和资源。
<dist:delivery>封装自定义模块分发的选项,如下所示。请注意,每个功能模块必须只配置这些自定义分发选项的一种类型。
<dist:install-time>指定模块应在安装时可用。对于未指定自定义分发选项的其他类型的功能模块,这是默认行为。
如需详细了解安装时下载,请参阅配置安装时分发
此节点还可以指定条件,用于限定要下载模块的设备所需满足的某些要求,例如设备功能,用户所在国家/地区或最低 API 级别。如需了解详情,请参阅配置按条件分发
<dist:removable dist:value="true | false" />当未设置或设置为 false 时,bundletool 会在根据 bundle 生成拆分 APK 时将安装时模块整合到基本模块中。 由于整合会使拆分 APK 的数量减少,因此此设置可以提升应用的性能。
removable 设置为 true 时:安装时模块将不会整合到基本模块中。如果您想要在将来卸载这些模块,请将其设置为 true。 不过,配置过多可移除的模块可能会导致应用的安装时间增加。
默认为 false。只有当您想要针对某个功能模块停用融合功能时,才需要在清单中设置此值。
注意:只有在使用 Android Gradle 插件 4.2 或从命令行使用 bundletool v1.0 时,才能使用此功能。
</dist:install-time>
<dist:on-demand/>指定应以按需下载的形式分发模块。也就是说,模块在安装时不会下载,但应用可以稍后请求下载。
如需详细了解按需下载,请参阅配置按需分发
</dist:delivery>
<application [android:hasCode](https://developer.android.google.cn/guide/topics/manifest/application-element#code)="true | false"> ... </application>如果功能模块没有生成 DEX 文件(也就是说,它不包含之后编译成 DEX 文件格式的代码),您必须执行以下操作(否则,您可能会遇到运行时错误):
1. 在功能模块的清单中将 android:hasCode 设置为 "false"
2. 将以下内容添加到基本模块的清单中:
<application<br> android:hasCode="true"<br> tools:replace="android:hasCode"><br> ...<br></application>

App主模块

  • 检查需要调用的模块是否安装
  • 如果没有安装则下载,这个过程是googlePlayCore库处理的
  • 如果已安装通过全类名进行调用访问
class MainActivity : AppCompatActivity() {
    val muduleName = "login_feature"
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val manager = SplitInstallManagerFactory.create(this)
        //安装模块监听
        manager.registerListener {
            val status=it.status()
            Log.d("TAG", it.toString())
        }

        loginModule.setOnClickListener {
            //模块已安装
            if (manager.installedModules.contains(muduleName)) {
                val intent = Intent()
                val componentName =
                    ComponentName(packageName, "cn.xxstudy.login_feature.LoginActivity")
                intent.component = componentName
                startActivity(intent)
            } else {
                //安装模块:
                GlobalScope.launch {
                    try {
                        manager.requestInstall(listOf(muduleName))
                    } catch (e: Exception) {
                        MainScope().launch {
                            Toast.makeText(this@MainActivity, "该模块还未安装成功", Toast.LENGTH_SHORT).show()
                        }
                    }
                }

            }
        }
    }

    override fun attachBaseContext(newBase: Context?) {
        super.attachBaseContext(newBase)
        //必须添加
        SplitCompat.install(this)
    }
}

manager.registerListener安装监听status说明:

SplitInstallSessionStatus.CANCELED -> 模块下载已被取消
SplitInstallSessionStatus.CANCELING -> 正在取消下载
SplitInstallSessionStatus.DOWNLOADING -> Installing(
        state.bytesDownloaded.toDouble() / state.totalBytesToDownload
)下载进度
SplitInstallSessionStatus.DOWNLOADED -> 下载完成但未安装
SplitInstallSessionStatus.FAILED ->下载或安装失败
SplitInstallSessionStatus.INSTALLED -> 安装完成
SplitInstallSessionStatus.INSTALLING -> 安装中
SplitInstallSessionStatus.PENDING -> 将要进行下载
SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION -> 需要用户确认,大于10M
SplitInstallSessionStatus.UNKNOWN -> 未知错误

本地构建调试

  1. 调试
    在这里插入图片描述
    接下来运行后login_fuature模块就不会被安装了,当然也是无法使用该模块的
    在这里插入图片描述
  2. 生成.aab
  • Build→Build Bundle(s)/ APK→Build Bundle(s)
  • Build→Generate Sigend Bundle Apk →Android App Bundle
  1. 通过BundleTool生成.apks
java -jar F:\Temp\bundletool.jar  build-apks --local-testing --bundle=app/build/outputs/bundle/debug/app-debug.aab --output=app.apks
  1. 安装
java -jar F:\Temp\bundletool.jar install-apks --apks=app.apks

编译过程:

E:\Android\Simple\Temp\PlayCoreDemo>java -jar F:\Temp\bundletool.jar  build-apks --local-testing --bundle=app/build/outputs/bundle/debug/app-debug.aab --output=app.apks
INFO: The APKs will be signed with the debug keystore found at 'C:\Users\DELL\.android\debug.keystore'.

E:\Android\Simple\Temp\PlayCoreDemo>java -jar F:\Temp\bundletool.jar install-apks --apks=app.apks
The APKs have been extracted in the directory: C:\Users\DELL\AppData\Local\Temp\1369014306226025145
The APKs have been extracted in the directory: C:\Users\DELL\AppData\Local\Temp\1369014306226025145
ADB << rm -rf '/sdcard/Android/data/cn.xxstudy.demo/files/local_testing'
ADB >> OK
ADB << mkdir -p '/sdcard/Android/data/cn.xxstudy.demo/files/local_testing' && rmdir '/sdcard/Android/data/cn.xxstudy.demo/files/local_testing' && mkdir -p '/sdcard/Android/data/cn.xxst
udy.demo/files/local_testing'
ADB >> OK
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-xxhdpi.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-master.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ca.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-da.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-fa.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ja.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ka.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-pa.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ta.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-nb.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-be.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-de.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ne.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-te.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-af.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-bg.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-th.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-fi.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-hi.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-si.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-vi.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-kk.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-mk.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-sk.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-uk.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-el.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-gl.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ml.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-nl.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-pl.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-sl.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-tl.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-am.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-km.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-bn.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-in.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-kn.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-mn.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ko.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-lo.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ro.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-sq.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ar.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-fr.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-hr.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-mr.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-or.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-sr.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-tr.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ur.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-as.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-bs.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-cs.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-es.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-is.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ms.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-et.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-it.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-lt.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-pt.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-eu.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-gu.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-hu.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ru.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-zu.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-lv.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-sv.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-iw.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-sw.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-hy.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ky.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-my.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-az.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-uz.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-en.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-zh.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/login_feature-xxhdpi.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/login_feature-master.apk"

这个时候就将login_feature模块导入到设备中去了,执行manager.requestInstall(listOf(muduleName))后就会进行安装了(一般是通过googlePlay下载,我们这里是使用BundleTool本地测试)
在这里插入图片描述

拆分注意事项

  1. dynamic-feature- moudle 引用base moudle 资源时,不能直接使用R.drawdble需要使用[base moudle packagename].R.drawdble的方式

  2. dynamic- feature-module项目名称不能以数字开头

  3. java.lo.lOException: Cannot find PROCESSED_ RES output for Main{type=MAIN, fullName=flavor1Debug, filters=I, versionCode=-1, versionName=null}异常需要注释掉build.gradlesplite {abi{}}

  4. base moudle不可以访问dynamic-feature-module中的id dynamic-feature-modulearssc文件中资源索引id的值为0x7e base moudlearssc文 件中资源索引id的值为0x7f因为featurebaseMoude都有各自的arsc文件,虽然属性名称一直 但是id值是不-致的,所以basemoude中涉及访问feature moudleid值都需要修改

  5. 动态模块配置模块名title必须通过如下方式dist:title="@string/title_ dynamic. feature"不能直接编写字符串,并且该字符串必须写在base moudle

  6. dynamic-feature moudlebase moudlemanifest文件最终会合并成一个manifest文件,所以要保障manifest的资源引用均在base moudle中。

  7. 当打开新建dynamic-feature moudle 并启用了on-demand(按需加载) 能力时,必须开启Fusing(熔断操作)才能正常的让Api21以下的设备正常使用module

  8. 一般情况下,动态模块下发之后需要重启App才能加载成功,但是如果你使用SplitCompat加载唤起动态模块,就可以立即生效

  9. 如果下载的模块太大,需要用户确认,googlePlay要求大于10MB需要用户确认

  10. dynamic- feature moudle 中的AndroidManifest中定义的Activity不能有exported:true因为别的app不知道你何时安装好模块从而会引发问题

问题

首次安装后不重启直接打开crash No package ID 7e found for ID 0x7e020000.

由于App 模块中arssc文件中资源索引id是0x7f 而feature module是0x7e 暂时还不知道具体原因

重启可解决!

2021-07-06 18:37:45.972 6029-6029/cn.xxstudy.demo E/cn.xxstudy.dem: No package ID 7e found for ID 0x7e020000.
2021-07-06 18:37:45.973 6029-6029/cn.xxstudy.demo D/AndroidRuntime: Shutting down VM
2021-07-06 18:37:45.974 6029-6029/cn.xxstudy.demo E/AndroidRuntime: FATAL EXCEPTION: main
    Process: cn.xxstudy.demo, PID: 6029
    java.lang.RuntimeException: Unable to start activity ComponentInfo{cn.xxstudy.demo/cn.xxstudy.login_feature.LoginActivity}: android.content.res.Resources$NotFoundException: Resource ID #0x7e020000
        ...
     Caused by: android.content.res.Resources$NotFoundException: Resource ID #0x7e020000
        at android.content.res.ResourcesImpl.getValue(ResourcesImpl.java:237)
        at android.content.res.Resources.loadXmlResourceParser(Resources.java:2281)
        at android.content.res.Resources.getLayout(Resources.java:1175)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:532)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:481)
        at androidx.appcompat.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:555)
        at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:161)
        at cn.xxstudy.login_feature.LoginActivity.onCreate(LoginActivity.kt:19)
        ...

结束语

至此App Bundle构建可动态化下载模块的App就结束了,其中可能会有很多坑,可以去官网看看
Google也已经发布说明在2021年8.1以后新应用上传到Google Play必须是.aab格式了,虽然国内无法使用但是还是要学习一下的,这里也放一个视频供大家参考(需要梯子)
AppBundle1
AppBundle2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吴唐人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值