Android 使用Google Core动态功能交付(Feature Delivery)

介绍

动态功能交付(Feature Delivery),是谷歌Google Play Core 库为谷歌商店发布的应用所提供的功能之一。

使用Play Core 库还可以提供以下这些功能:

  • 下载其他语言资源
  • 管理功能模块分发
  • 管理资源包分发
  • 触发应用内更新
  • 请求应用内评价

本文的重点是说明功能模块分发与资源包分发的使用。

Android App Bundle

Android App Bundle 是一种发布格式(.aab),这种格式和传统的apk类似,都会包含应用的所有经过编译的代码和资源。

不同之处在于将这种格式不能直接安装,而开发者需要将包上传至Google Play之后,Google Play会使用这个 App Bundle 包针对每种设备配置生成并提供经过优化的 APK,即 APK 的生成及签名交由了 Google Play 来完成。

如果要使用动态功能交付或资产交付,则必须要使用App Bundle方式打包。

如上图所示,传统的apk打包是将这些组成部分打包成一个压缩包,即apk包。而app bundle打包的aab文件也是包含以上所有内容,但是在将这个aab格式的包上传至Google Play后,用户进入商店选择下载应用时,下载的包将不是包含所有内容的包,而是只包含该用户手机所对应的相应类型资源的包。

如该用户的手机是使用英语、xhdpi、arm-v7架构cpu的设备,那么该用户下载到的包将只包含(如果你启用了这些类型的拆分)这些类型对应的语言、资源图、lib库文件。显而易见,这种方式将有助于缩小用户下载的包大小与减小应用所占用的空间。

而将现有的项目修改为app bundle打包的方法也很简单:

  1. 在项目gradle中引入google core库

    // In your app’s build.gradle file:
    ...
    dependencies {
        // This dependency is downloaded from the Google’s Maven repository.
        // So, make sure you also include that repository in your project's build.gradle file.
        implementation 'com.google.android.play:core:1.9.1'
    
        // For Kotlin users also import the Kotlin extensions library for Play Core:
        implementation 'com.google.android.play:core-ktx:1.8.1'
        ...
    }
    
  2. 在项目的app模块的build.gradle文件中添加以下内容:

android { // 在gradle的android结点下
    
    bundle {
        density {
            // Different APKs are generated for devices with different screen densities; true by default.
            enableSplit true
        }
        abi {
            // Different APKs are generated for devices with different CPU architectures; true by default.
            enableSplit true
        }
        language {
            // This is disabled so that the App Bundle does NOT split the APK for each language.
            // We're gonna use the same APK for all languages.
            enableSplit true
        }
    }
}
  1. 在打包时选择app bundle打包即可

这样打出来的包就是aab格式的包,发版时将这个aab文件传到谷歌后台即可。如果需要测试这个aab包的功能,则需要使用谷歌的bundletool工具来安装aab包到测试机上。

资产交付(Asset Delivery)

通常,对于用户不会立刻使用到的一些比较占用空间的assets资源,如大量图片资源、机器学习模型参数包、较大的视频资源等,可以使用动态资产交付。

具体的使用方式为,将这些资源单独拆分在一个Module的assets文件夹下,并将该Module设置为动态分发的模块。

动态资产交付主要有三种交付模式,分别为:

  • install-time:安装时分发,应用安装时就会下载,安装后可立即使用,资产包的大小会计入应用详情页的下载大小
  • fast-follow:快速跟进式分发,应用安装后会立即开始下载,安装后不一定保证可以使用,资产包大小暂时还不会计入详情页大小
  • on-demand:按需分发,资产包不会主动下载,需要代码在特定时机向用户征求开始下载,确保下载后才可使用,资产包大小不会计入详情页大小

这三种交付模式可以在资源Module的AndroidManifest.xml中设置。

代码中,对于动态分发的资源的访问,install-time与另外两种模式不同,对于install-time的模块,与常规资源一样使用AssertManager访问,官方代码示例为:

import android.content.res.AssetManager;
...
Context context = createPackageContext("com.example.app", 0);
AssetManager assetManager = context.getAssets();
InputStream is = assetManager.open("asset-name");

而对于另外两种模式,要访问动态分发模块的资源,则要复杂许多。

首先需要AssetPackManager#getPackLocation()方法获取 Asset Pack 的根文件夹。如果返回值存在,则说明动态模块可用,如果为null,则需要代码申请下载并安装该模块才可用。流程如下图,由于不是本文重点,故详细代码不列出来了。具体可以查看官方文档

动态功能交付(Feature Delivery)

介绍

动态功能交付与资产交付类似,同样是需要单独拆分出一个模块,而且这个模块与常规的功能模块存在很大的不同。

我们通常添加一个模块,是希望这个模块中的代码能被多个项目复用,所以app模块与这个feature模块的引用关系为:app引用feature模块。

这种引用关系决定了app模块中可以随意使用feature模块中的代码和资源,而feature模块无法访问app中的资源和代码。

在动态功能交付中,这种引用关系需要反过来,feature模块引用app模块。所以feature中可以随意访问app中的代码和资源,而反过来则不行。

接下来,我们演示一下将一部分代码和资源从原有的项目中拆分出来。

在我负责的项目中,由于opencv的so文件占用的空间太大,产品不满意,所以决定将openCV对应的so文件和相应的activity页面与功能代码拆分出来配置成动态功能交付。

使用方式

首先,先新建一个Module,可以直接在AS中选择创建动态功能交付的Module,然后根据提示填好Module的Name和Title。

创建完成之后,确认以下配置:

app的gradle文件中,以动态模块的形式引入了刚刚创建的模块

android { // android结点下
    
    dynamicFeatures = [':opencvfeature']
}

创建模块的gradle文件中,引入了app模块

dependencies {
    implementation project(":app")
}

新创建的模块AndroidManifest.xml文件中,已经添加了以下内容:

    <dist:module dist:title="@string/module_name">
        <dist:delivery>
            <dist:on-demand />
        </dist:delivery>
        <dist:fusing dist:include="true" />
    </dist:module>

确认以上内容后,一个动态功能交付的模块就创建完成了。接下来要做的工作就是将原app模块中需要拆分的代码迁移到新创建的这个模块中,并消除app模块对这部分代码的引用(即能成功编译)。这部分工作和每个人的项目相关,有些甚至需要重构功能代码,所以就不展开讲了。

在这个过程中可能会发现一个问题,那就是app模块无法引用动态模块中的代码和资源后,那动态模块我要如何使用呢?无法引用其中的类,也就无法调用其中的功能方法,那动态分发的意义在哪里?

这个问题的答案也很简答:使用反射。

对于调用动态模块中的代码,使用反射的方式去调用对应类中的方法。对于跳转动态模块中的activity,使用Intent指定类名和包名即可,示例如下。

Intent intent = new Intent();
intent.setClassName(getPackageName(), "com.xxx.xxx.XXXActivity");
startActivityForResult(intent, requestCode);

修改代码完成后,工程目录大致如下,其中opencvfeature就是我拆分出的动态功能模块。

完成了以上工作,还有一个重要的工作没有完成,即使用该模块的功能前,判断模块是否安装以及申请下载安装该模块,毕竟我们的功能模块设置的是on-demand(按需交付)模式。

private SplitInstallManager splitInstallManager;

@Override
protected void onCreate(Bundle savedInstanceState) {
	// ...
    splitInstallManager = SplitInstallManagerFactory.create(this); // 创建manager
}

@Override
public void onResume() {
	// ...
    splitInstallManager.registerListener(splitInstallStateUpdatedListener);
}

 @Override
public void onPause() {
    // ...
    splitInstallManager.unregisterListener(splitInstallStateUpdatedListener);
}

// 使用动态模块功能
public void tryUseDynamicFeature() {
    String moduleName = FeatureManager.getInstance().getModuleName();
    if (splitInstallManager.getInstalledModules().contains(moduleName)) {
        // 已下载对应的模块
        // TODO 使用反射代码
        
    } else { // 没有下载模块,需要申请下载
        SplitInstallRequest request = SplitInstallRequest.newBuilder().addModule(moduleName).build();
        splitInstallManager.startInstall(request);

        boolean isGoogleCanUse = PhoneUtil.isGooglePlayServiceAvailable(this);
        if (isGoogleCanUse) {
            T.show("Starting install for " + moduleName);
        } else {
            T.show("The Google service appears to be unavailable and the installation cannot begin");
        }
    }
}

// 动态模块的状态监听
private final SplitInstallStateUpdatedListener splitInstallStateUpdatedListener = state -> {
        switch (state.status()) {
            case SplitInstallSessionStatus.DOWNLOADING: {
                // 正在下载
            }
            break;
            case SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION: {
                // 需要用户确认
            }
            break;
            case SplitInstallSessionStatus.INSTALLED: {
                // 安装模块成功
            }
            break;
            case SplitInstallSessionStatus.INSTALLING: {
                // 正在安装
            }
            break;
            case SplitInstallSessionStatus.FAILED: {
                // 下载模块失败
            }
            break;
            default:
                break;
        }
    };
动态交付的优势
  • 减少必要apk的体积,使得在流量分发中更具优势
  • 使用谷歌官方服务器分发,稳定性更有保障,不需要消耗宝贵的服务器资源
  • 下载过程由google自身处理,支持断点续传等
注意事项
  • 动态功能交付和资产交付不同,只有两种模式,install-time和on-demand,没有提供fast-follow
  • 测试动态功能交付需要使用Google Play提供的测试服功能,将包当作测试包传上商店,将测试的账号加入测试账号列表,即可测试
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
好的,下面是使用ZXing实现Android扫码功能的示例代码。 首先,添加ZXing依赖库到你的项目中。可以使用Gradle,在build.gradle文件中添加以下代码: ``` implementation 'com.google.zxing:core:3.4.0' implementation 'com.journeyapps:zxing-android-embedded:3.6.0' ``` 然后,在你的layout文件中添加一个用于预览扫描结果的SurfaceView: ```xml <SurfaceView android:id="@+id/surfaceView" android:layout_width="match_parent" android:layout_height="match_parent" /> ``` 接下来,在你的Activity中实现扫描逻辑。首先,初始化扫描器并启动扫描: ```java import android.Manifest; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import com.google.zxing.BarcodeFormat; import com.google.zxing.Result; import com.journeyapps.barcodescanner.BarcodeCallback; import com.journeyapps.barcodescanner.BarcodeEncoder; import com.journeyapps.barcodescanner.BarcodeResult; import com.journeyapps.barcodescanner.CaptureManager; import com.journeyapps.barcodescanner.DecoratedBarcodeView; import java.util.ArrayList; import java.util.List; public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback { private static final String TAG = MainActivity.class.getSimpleName(); private static final int REQUEST_CAMERA_PERMISSION = 101; private CaptureManager capture; private DecoratedBarcodeView barcodeScannerView; private SurfaceView surfaceView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); surfaceView = findViewById(R.id.surfaceView); barcodeScannerView = findViewById(R.id.barcodeScannerView); // Check camera permission if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION); } else { startCamera(); } } private void startCamera() { barcodeScannerView.getBarcodeView().setDecoderFactory(new DefaultDecoderFactory(getBarcodeFormats())); capture = new CaptureManager(this, barcodeScannerView); capture.initializeFromIntent(getIntent(), savedInstanceState); capture.decode(); } private List<BarcodeFormat> getBarcodeFormats() { List<BarcodeFormat> formats = new ArrayList<>(); formats.add(BarcodeFormat.QR_CODE); return formats; } @Override protected void onResume() { super.onResume(); if (capture != null) { capture.onResume(); } } @Override protected void onPause() { super.onPause(); if (capture != null) { capture.onPause(); } } @Override protected void onDestroy() { super.onDestroy(); if (capture != null) { capture.onDestroy(); } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (capture != null) { capture.onSaveInstanceState(outState); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == REQUEST_CAMERA_PERMISSION) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { startCamera(); } else { Toast.makeText(this, "Camera permission required", Toast.LENGTH_SHORT).show(); } } } @Override public void surfaceCreated(SurfaceHolder holder) { if (capture != null) { capture.onSurfaceCreated(holder); } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { if (capture != null) { capture.onSurfaceChanged(holder, format, width, height); } } @Override public void surfaceDestroyed(SurfaceHolder holder) { if (capture != null) { capture.onSurfaceDestroyed(holder); } } } ``` 这里使用了ZXing官方提供的CaptureManager和DecoratedBarcodeView来处理扫描逻辑和预览界面。在onCreate方法中,首先检查相机权限,然后初始化扫描器并启动扫描。在onResume、onPause、onDestroy、onSaveInstanceState等方法中,分别处理扫描器的生命周期。 最后,在AndroidManifest.xml文件中添加相机权限: ```xml <uses-permission android:name="android.permission.CAMERA" /> ``` 这样,就可以实现基本的扫码功能了。当用户扫描了一张二维码后,可以在onActivityResult方法中获取扫描结果: ```java @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_SCAN) { String result = data.getStringExtra(Intents.Scan.RESULT); Log.d(TAG, "Scan result: " + result); } } ``` 完整的示例代码可以参考这个GitHub项目:https://github.com/journeyapps/zxing-android-embedded

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值