You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter开发XAPK安装器:如何正确安装XAPK中的所有APK?

如何在Flutter中正确安装XAPK文件中的所有APK?

我用Flutter开发XAPK安装器,借助open_file包能唤起普通APK的安装对话框,但XAPK包含base APK、OBB文件和多个附加APK。解压后仅安装base APK时,应用无法正常运行,因为未完成所有附加APK的配套安装。请问如何正确安装XAPK文件中的所有APK?

现有Dart代码如下:

import 'dart:io';

import 'package:archive/archive.dart';
import 'package:package_archive_info/package_archive_info.dart';
import 'package:path_provider/path_provider.dart';

abstract class XapkInstaller {  
  static install({ required String apkPath }) async {
    late List<FileSystemEntity> allFiles, apkFiles;
    late PackageArchiveInfo appInfo;
    late String appPackageName;

    Directory tempDir = await getTemporaryDirectory();
    String tempPath = tempDir.path;

    String appName = apkPath.split("/").last.replaceAll(".xapk", "");
    String zipFilePath = tempDir.path + "/${appName}_zip";
    // 转换XAPK为Zip并移动到指定目录
    _moveFile(File(apkPath), zipFilePath, appName);

    final bytes = File(zipFilePath + "/$appName" + ".zip").readAsBytesSync();
    final archive = ZipDecoder().decodeBytes(bytes);

    // 解压Zip内容到临时目录
    for (final file in archive) {
      final String filename = file.name;

      if (file.isFile) {
        final data = file.content as List<int>;
        File(tempDir.path + "/$appName" + "/$filename")
          ..createSync(recursive: true)
          ..writeAsBytesSync(data);
      } else {
        Directory(tempPath).create(recursive: true);
      }
    }
    final Directory myDir = Directory(tempDir.path + "/$appName");

    allFiles = myDir.listSync(recursive: true, followLinks: true);

    apkFiles = allFiles.where((element) => element.path.endsWith('.apk')).toList();
    for (int x = 0; x < apkFiles.length; x++) {
      final String filePath = apkFiles[x].path;
      try {
        appInfo = await PackageArchiveInfo.fromPath(filePath);
        appPackageName = appInfo.packageName;
      } catch (e) {
        appInfo = PackageArchiveInfo(
            appName: "", packageName: "", version: "", buildNumber: "");
      }
      if (appInfo.appName.isNotEmpty &&
          appPackageName == App.apkName(apkPath: filePath)) {
        try {
          // 将base APK复制到指定目录
          File(filePath)
              .copySync(await App.getApksDirectory() + "/$appName.apk");

          // 移动OBB文件到Android/obb目录
          _moveObbToAndroidDir(allFiles, appPackageName);

          // 唤起安装对话框(仅安装base APK)
          await OpenFile.open(filePath);

          // 删除原XAPK文件
          File(await App.getApksDirectory() + "/$appName" + ".xapk").delete();
        } catch (e) {
          // 安装错误处理
        }
      }
    }
    // 180秒后清理临时文件
    Future.delayed(const Duration(seconds: 180), () {
      tempDir.deleteSync(recursive: true);
      tempDir.create();
    });  
  }

  static _moveFile(File sourceFile, String newPath, String appname) async {
    if (!Directory(newPath).existsSync()) Directory(newPath).createSync();
    final String zipFilePath = "$newPath/" + appname + ".zip";

    try {
      return sourceFile.copySync(zipFilePath);
    } on FileSystemException {
      // 复制并删除原文件
      await sourceFile.copy(zipFilePath);
      await sourceFile.delete();
    }  
  }

  static _moveObbToAndroidDir(
      List<FileSystemEntity> allFiles, String appPackageName) async {
    for (int x = 0; x < allFiles.length; x++) {
      final fileExtension = allFiles[x].path.split("/").last.split(".").last;

      if (fileExtension == "obb") {
        String filepath = allFiles[x].path;
        String obbFileName = filepath.split("/").last.split(".").first;

        String obbDirPath = await App.internalStoragePath() +
            "/Android" +
            "/obb" +
            "/$appPackageName";

        // 创建OBB目录
        if (!Directory(obbDirPath).existsSync()) {
          Directory(obbDirPath).createSync();
        }

        // 目标OBB文件路径
        final String renamePath = obbDirPath + "/" + obbFileName + ".obb";

        try {
          // 同步复制
          File(filepath).copySync(renamePath);
        } on FileSystemException {
          // 异步复制
          await File(filepath).copy(renamePath);
        }
      }
    }  
  } 
}

问题分析

当前代码的核心问题是:仅调用OpenFile.open()安装base APK,但XAPK中的**split APK(附加APK)**未被处理。Android系统对包含split APK的应用,要求一次性安装所有关联APK文件,单独安装base APK会导致应用缺失必要组件,无法正常运行。

解决方案

要正确安装XAPK中的所有APK,需使用Android原生PackageInstaller API批量安装多个APK文件。Flutter本身无直接支持,需通过MethodChannel调用Android原生代码实现。

步骤1:修改Dart代码,收集APK路径并调用原生方法

保留原有解压、OBB移动逻辑,新增MethodChannel调用原生安装方法:

import 'dart:io';
import 'package:flutter/services.dart';
import 'package:archive/archive.dart';
import 'package:package_archive_info/package_archive_info.dart';
import 'package:path_provider/path_provider.dart';

abstract class XapkInstaller {
  static const MethodChannel _channel = MethodChannel('xapk_installer');

  static install({ required String apkPath }) async {
    late List<FileSystemEntity> allFiles, apkFiles;
    late String appPackageName;

    Directory tempDir = await getTemporaryDirectory();
    String appName = apkPath.split("/").last.replaceAll(".xapk", "");
    String zipFilePath = tempDir.path + "/${appName}_zip";

    // 转换XAPK为Zip并解压
    _moveFile(File(apkPath), zipFilePath, appName);
    final bytes = File(zipFilePath + "/$appName" + ".zip").readAsBytesSync();
    final archive = ZipDecoder().decodeBytes(bytes);

    for (final file in archive) {
      final String filename = file.name;
      if (file.isFile) {
        final data = file.content as List<int>;
        File(tempDir.path + "/$appName" + "/$filename")
          ..createSync(recursive: true)
          ..writeAsBytesSync(data);
      } else {
        Directory(tempDir.path).createSync(recursive: true);
      }
    }

    // 收集所有APK路径
    final Directory myDir = Directory(tempDir.path + "/$appName");
    allFiles = myDir.listSync(recursive: true, followLinks: true);
    apkFiles = allFiles.where((element) => element.path.endsWith('.apk')).toList();
    List<String> apkPaths = apkFiles.map((file) => file.path).toList();

    try {
      // 获取应用包名
      PackageArchiveInfo info = await PackageArchiveInfo.fromPath(apkPaths.first);
      appPackageName = info.packageName;

      // 移动OBB文件
      _moveObbToAndroidDir(allFiles, appPackageName);

      // 调用原生批量安装方法
      bool installSuccess = await _channel.invokeMethod('installMultipleApks', {
        'apkPaths': apkPaths,
        'packageName': appPackageName,
      });

      if (installSuccess) {
        // 安装成功后删除原XAPK
        File(await App.getApksDirectory() + "/$appName.xapk").delete();
      }
    } catch (e) {
      // 错误处理
    }

    // 180秒后清理临时文件
    Future.delayed(const Duration(seconds: 180), () {
      tempDir.deleteSync(recursive: true);
      tempDir.create();
    });
  }

  // 保留原有_moveFile、_moveObbToAndroidDir方法
  static _moveFile(File sourceFile, String newPath, String appname) async {
    if (!Directory(newPath).existsSync()) Directory(newPath).createSync();
    final String zipFilePath = "$newPath/$appname.zip";
    try {
      return sourceFile.copySync(zipFilePath);
    } on FileSystemException {
      await sourceFile.copy(zipFilePath);
      await sourceFile.delete();
    }
  }

  static _moveObbToAndroidDir(
      List<FileSystemEntity> allFiles, String appPackageName) async {
    for (var file in allFiles) {
      final fileExtension = file.path.split("/").last.split(".").last;
      if (fileExtension == "obb") {
        String filepath = file.path;
        String obbFileName = filepath.split("/").last;
        String obbDirPath = await App.internalStoragePath() + "/Android/obb/$appPackageName";

        if (!Directory(obbDirPath).existsSync()) {
          Directory(obbDirPath).createSync();
        }

        final String targetPath = obbDirPath + "/$obbFileName";
        try {
          File(filepath).copySync(targetPath);
        } on FileSystemException {
          await File(filepath).copy(targetPath);
        }
      }
    }
  }
}

步骤2:实现Android原生安装逻辑

在Android项目中创建XapkInstallerPlugin类,处理MethodChannel调用:

package com.your.package.name;

import android.app.PendingIntent;
import android.content.Intent;
import android.content.pm.PackageInstaller;
import android.os.Build;
import android.util.Log;
import androidx.core.content.FileProvider;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;

public class XapkInstallerPlugin implements FlutterPlugin, MethodChannel.MethodCallHandler {
  private static final String CHANNEL_NAME = "xapk_installer";
  private MethodChannel channel;

  @Override
  public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {
    channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), CHANNEL_NAME);
    channel.setMethodCallHandler(this);
  }

  @Override
  public void onMethodCall(MethodCall call, MethodChannel.Result result) {
    if (call.method.equals("installMultipleApks")) {
      List<String> apkPaths = call.<List<String>>argument("apkPaths");
      String packageName = call.<String>argument("packageName");
      installMultipleApks(apkPaths, packageName, result);
    } else {
      result.notImplemented();
    }
  }

  private void installMultipleApks(List<String> apkPaths, String packageName, MethodChannel.Result result) {
    PackageInstaller packageInstaller = channel.getFlutterView().getContext().getPackageManager().getPackageInstaller();
    PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
    params.setAppPackageName(packageName);

    int sessionId = 0;
    try {
      sessionId = packageInstaller.createSession(params);
      PackageInstaller.Session session = packageInstaller.openSession(sessionId);

      // 将所有APK写入安装会话
      for (String apkPath : apkPaths) {
        addApkToSession(session, apkPath);
      }

      // 创建安装回调Intent
      Intent intent = new Intent(channel.getFlutterView().getContext(), InstallResultReceiver.class);
      PendingIntent pendingIntent = PendingIntent.getBroadcast(
          channel.getFlutterView().getContext(),
          sessionId,
          intent,
          Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE : PendingIntent.FLAG_UPDATE_CURRENT
      );

      // 提交安装会话
      session.commit(pendingIntent.getIntentSender());
      session.close();
      result.success(true);
    } catch (IOException e) {
      result.error("INSTALL_ERROR", e.getMessage(), null);
      if (sessionId != 0) {
        packageInstaller.abandonSession(sessionId);
      }
    }
  }

  private void addApkToSession(PackageInstaller.Session session, String apkPath) throws IOException {
    File apkFile = new File(apkPath);
    try (InputStream inputStream = new FileInputStream(apkFile)) {
      OutputStream outputStream = session.openWrite(apkFile.getName(), 0, apkFile.length());
      byte[] buffer = new byte[1024 * 1024];
      int length;
      while ((length = inputStream.read(buffer)) != -1) {
        outputStream.write(buffer, 0, length);
      }
      session.fsync(outputStream);
      outputStream.close();
    }
  }

  @Override
  public void onDetachedFromEngine(FlutterPluginBinding binding) {
    channel.setMethodCallHandler(null);
  }
}

步骤3:创建安装结果广播接收器

创建InstallResultReceiver类接收安装状态:

package com.your.package.name;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInstaller;
import android.widget.Toast;

public class InstallResultReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
    int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE);
    String message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);

    switch (status) {
      case PackageInstaller.STATUS_SUCCESS:
        Toast.makeText(context, "安装成功", Toast.LENGTH_LONG).show();
        break;
      case PackageInstaller.STATUS_FAILURE:
      case PackageInstaller.STATUS_FAILURE_ABORTED:
      case PackageInstaller.STATUS_FAILURE_BLOCKED:
      case PackageInstaller.STATUS_FAILURE_CONFLICT:
      case PackageInstaller.STATUS_FAILURE_INCOMPATIBLE:
      case PackageInstaller.STATUS_FAILURE_INVALID:
      case PackageInstaller.STATUS_FAILURE_STORAGE:
        Toast.makeText(context, "安装失败: " + message, Toast.LENGTH_LONG).show();
        break;
    }
  }
}

步骤4:配置AndroidManifest.xml

添加必要权限和组件声明:

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />

<application ...>
  <!-- 注册广播接收器 -->
  <receiver android:name=".InstallResultReceiver" />

  <!-- 配置FileProvider -->
  <provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
      android:name="android.support.FILE_PROVIDER_PATHS"
      android:resource="@xml/file_paths" />
  </provider>
</application>

在res/xml目录创建file_paths.xml:

<?xml version="1.0" encoding="utf-8"?>
<paths>
  <external-cache-path name="external_cache" path="." />
  <cache-path name="cache" path="." />
</paths>

关键注意事项

  1. 权限处理:需动态申请REQUEST_INSTALL_PACKAGES权限(Android 8.0+),以及存储权限(Android 13以下)。
  2. FileProvider配置:必须正确配置,否则Android 7.0+无法访问APK文件。
  3. 会话提交:所有APK需写入同一个安装会话后一次性提交,才能保证base和split APK被正确关联安装。

内容的提问来源于stack exchange,提问作者Sonu Lodha

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.23 08:06:20