4. SplitInstallAsyncTask 构造函数:参数说明、Source 与 Listener
构造函数是 SplitInstallAsyncTask 实例化的唯一入口,它接收两个强制参数:Source 和 Listener。理解这两个参数的类型、行为以及它们如何影响任务生命周期,是正确使用该 API 的前提。
4.1 构造函数签名
无论使用 Java 还是 Kotlin,构造函数定义均为:
public SplitInstallAsyncTask(Source source, Listener listener)
- source — 提供拆分模块(Split)的原始数据来源,例如 Play Asset Delivery 的 ZIP 流、本地 APK 文件或远程下载的拆分包。
- listener — 回调接口,用于接收安装进度、成功、失败等事件。
建议:构造方法本身不会启动任何网络或 I/O 操作,仅执行参数校验与内部状态初始化。实际耗时逻辑在调用
execute() 后才触发。
4.2 参数详解
| 参数 | 类型 | 说明 | 关键约束 |
|---|---|---|---|
| source | Source(抽象类/接口) |
封装拆分包的字节流提供方,必须实现 openStream() 或 openFile() 方法。 |
不允许为 null;流需可重复读取(支持重试) |
| listener | Listener(接口) |
回调方法集合,包括 onStart()、onProgress()、onComplete()、onError() 等。 |
不允许为 null;回调默认在异步线程执行 |
4.3 Source 详解
Source 定义了数据读取契约。常见实现包括:
- FileSource — 基于本地文件系统读取拆分 APK。
- AssetPackSource — 从 Play Asset Delivery 的 Asset Pack 中读取。
- StreamSource — 从网络 URL 或自定义 InputStream 读取。
所有 Source 必须实现以下核心方法:
public interface Source {
InputStream openStream() throws IOException;
long getLength(); // 返回数据总长度,用于进度计算
String getSplitName(); // 返回拆分模块名称,例如 "config.zh"
}
实现注意事项:
openStream() 可能在任务重试时被多次调用,必须保证每次返回一个全新的、可独立读取的流。避免使用单次消费的流(如 ByteArrayInputStream 的副本实现除外)。
4.4 Listener 详解
Listener 是异步通知的桥梁,定义如下:
public interface Listener {
void onStart(SplitInstallAsyncTask task);
void onProgress(SplitInstallAsyncTask task, long bytesRead, long totalBytes);
void onComplete(SplitInstallAsyncTask task, File installedFile);
void onError(SplitInstallAsyncTask task, Exception error);
}
- onStart — 任务开始执行时回调,可用于展示 UI 加载状态。
- onProgress — 每处理一个数据块后回调,适合显示进度条。
- onComplete — 拆分模块成功安装后回调,返回安装后的文件路径。
- onError — 任何异常(网络错误、磁盘空间不足、流损坏等)均通过此回调返回。
注意:所有回调默认在内部工作线程池中执行。如果需要在主线程更新 UI,必须在 Listener 内部使用
Handler 或 runOnUiThread() 切换线程。直接在此回调中操作 View 会抛出 CalledFromWrongThreadException。
4.5 构造函数调用示例
// 创建一个本地文件 Source
Source source = new FileSource(new File(getSplitDir(), "config.zh.apk"));
// 自定义 Listener
Listener listener = new Listener() {
@Override
public void onStart(SplitInstallAsyncTask task) {
Log.d("SplitInstall", "任务开始");
}
@Override
public void onProgress(SplitInstallAsyncTask task, long read, long total) {
int percent = (int) (read * 100 / total);
Log.d("SplitInstall", "进度: " + percent + "%");
}
@Override
public void onComplete(SplitInstallAsyncTask task, File installedFile) {
Log.d("SplitInstall", "安装成功: " + installedFile.getAbsolutePath());
}
@Override
public void onError(SplitInstallAsyncTask task, Exception error) {
Log.e("SplitInstall", "安装失败", error);
}
};
// 构造任务对象
SplitInstallAsyncTask task = new SplitInstallAsyncTask(source, listener);
// 后续调用 task.execute() 启动...
4.6 构造函数的内部流程
下图展示了构造函数如何将外部传入的 Source 和 Listener 装配到任务实例中,并为后续执行阶段铺平道路:
4.7 常见错误与避坑
- 空指针异常:
source或listener为null时,构造函数会立即抛出NullPointerException。务必在调用前校验。 - 不可重复读取的流:若
Source.openStream()返回的流只能读取一次(如SocketInputStream),当任务因重试机制再次调用时将失败。推荐使用BufferedInputStream或多态缓存。 - 在 UI 线程调用构造函数:构造函数本身开销很小,但不建议在频繁 UI 帧中执行。若确需如此,确保
Source的构造方法(如FileSource)不包含 I/O 操作。 - Listener 未处理异常:
onError()中应至少打印日志,否则异常会被静默吞掉,导致诡异的安装失败。
最佳实践:将 Source 和 Listener 的创建逻辑封装在工厂方法或依赖注入中,便于单元测试和替换实现。例如用 MockSource 模拟流错误以验证重试逻辑。
4.8 调试与验证
在构造函数内添加日志有助于快速排查入参问题:
// 可在 SplitInstallAsyncTask 构造函数中(实际内部实现):
Log.d("SplitInstall", "Construct invoked. source=" + source.getClass().getName()
+ ", listener=" + listener.getClass().getName());
通过 ADB 日志过滤 SplitInstall 标签,观察构造是否成功。如果日志未出现,检查是否实例化时传入的 source 或 listener 被提前回收(极少出现,多见于匿名内部类导致的内存泄漏)。