【Android】【Package】安装split apk
1 背景
如果要安装https://apkpure.com/flipboard-your-social-magazine/flipboard.app/download
这种链接下载的apk,因为下载下来的是xapk文件格式。安装方法有下面几种。
2 用apkpure的app去安装
按照apkpure网站写的教程即可,略。
3 自己安装
将xapk重命名成zip文件,解压缩后文件如下:
-rw-rw-r-- 1 suo suo 135577 12月 31 1979 config.ar.apk
-rw-rw-r-- 1 suo suo 2855495 12月 31 1979 config.arm64_v8a.apk
-rw-rw-r-- 1 suo suo 119193 12月 31 1979 config.de.apk
-rw-rw-r-- 1 suo suo 65945 12月 31 1979 config.en.apk
-rw-rw-r-- 1 suo suo 151961 12月 31 1979 config.es.apk
-rw-rw-r-- 1 suo suo 119193 12月 31 1979 config.fr.apk
-rw-rw-r-- 1 suo suo 115097 12月 31 1979 config.it.apk
-rw-rw-r-- 1 suo suo 127385 12月 31 1979 config.ja.apk
-rw-rw-r-- 1 suo suo 119193 12月 31 1979 config.ko.apk
-rw-rw-r-- 1 suo suo 123289 12月 31 1979 config.pt.apk
-rw-rw-r-- 1 suo suo 135577 12月 31 1979 config.ru.apk
-rw-rw-r-- 1 suo suo 119193 12月 31 1979 config.tr.apk
-rw-rw-r-- 1 suo suo 326413 12月 31 1979 config.xxxhdpi.apk
-rw-rw-r-- 1 suo suo 39365043 12月 31 1979 flipboard.app.apk
-rw-rw-r-- 1 suo suo 1915 12月 31 1979 icon.png
-rw-rw-r-- 1 suo suo 1983 12月 31 1979 manifest.json
这是海外Android应用广泛使用的技术,将一个完整的apk按locale、so库拆分(这只是我的理解,具体需要继续研究),拆分信息如下:
manifest.json
{
"xapk_version": 2,
"package_name": "flipboard.app",
"name": "Flipboard",
"version_code": "5490",
"version_name": "4.3.32",
"min_sdk_version": "21",
"target_sdk_version": "35",
"permissions": [
"flipboard.app.permission.C2D_MESSAGE",
"android.permission.INTERNET",
"android.permission.ACCESS_NETWORK_STATE",
"com.google.android.c2dm.permission.RECEIVE",
"android.permission.WRITE_EXTERNAL_STORAGE",
"android.permission.WAKE_LOCK",
"android.permission.FOREGROUND_SERVICE",
"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK",
"android.permission.ACCESS_WIFI_STATE",
"com.google.android.gms.permission.AD_ID",
"android.permission.ACCESS_COARSE_LOCATION",
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.ACCESS_ADSERVICES_ATTRIBUTION",
"android.permission.ACCESS_ADSERVICES_AD_ID",
"android.permission.AD_SERVICES_CONFIG",
"android.permission.DOWNLOAD_WITHOUT_NOTIFICATION",
"android.permission.POST_NOTIFICATIONS",
"com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE",
"android.permission.ACCESS_ADSERVICES_TOPICS",
"android.permission.ACCESS_ADSERVICES_CUSTOM_AUDIENCE",
"flipboard.app.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
],
"split_configs": [
"config.xxxhdpi",
"config.de",
"config.tr",
"config.pt",
"config.en",
"config.arm64_v8a",
"config.ru",
"config.ar",
"config.ja",
"config.ko",
"config.fr",
"config.es",
"config.it"
],
"total_size": 43878554,
"icon": "icon.png",
"split_apks": [
{
"file": "flipboard.app.apk",
"id": "base"
},
{
"file": "config.xxxhdpi.apk",
"id": "config.xxxhdpi"
},
{
"file": "config.de.apk",
"id": "config.de"
},
{
"file": "config.tr.apk",
"id": "config.tr"
},
{
"file": "config.pt.apk",
"id": "config.pt"
},
{
"file": "config.en.apk",
"id": "config.en"
},
{
"file": "config.arm64_v8a.apk",
"id": "config.arm64_v8a"
},
{
"file": "config.ru.apk",
"id": "config.ru"
},
{
"file": "config.ar.apk",
"id": "config.ar"
},
{
"file": "config.ja.apk",
"id": "config.ja"
},
{
"file": "config.ko.apk",
"id": "config.ko"
},
{
"file": "config.fr.apk",
"id": "config.fr"
},
{
"file": "config.es.apk",
"id": "config.es"
},
{
"file": "config.it.apk",
"id": "config.it"
}
]
}
这些拆分出来的apk必需是相同的包名和签名,才能安装成功。
3.1 adb命令安装
adb install-multiple config.ar.apk config.de.apk config.es.apk config.it.apk config.ko.apk config.ru.apk config.xxxhdpi.apk config.arm64_v8a.apk config.en.apk config.fr.apk config.ja.apk config.pt.apk config.tr.apk flipboard.app.apk
3.2 PackageInstaller的接口(程序)安装
package xxx;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.OutputStream;
@SuppressLint({"SdCardPath", "LogNotTimber", "UnspecifiedImmutableFlag"})
public class MultipleInstaller {
private static final String TAG = "MultipleInstaller";
private final Context mContext;
private Handler mHandler;
private PackageManager mPackageManager;
private PackageInstaller mPackageInstaller;
public MultipleInstaller(Context mContext) {
this.mContext = mContext;
mPackageManager = this.mContext.getPackageManager();
mPackageInstaller = mPackageManager.getPackageInstaller();
}
static class MultiInstallReceiver extends BroadcastReceiver {
public static final String ACTION = "duoping.multiple.install.action";
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "MultiInstallReceiver onReceive");
}
}
public void install() {
HandlerThread thread = new HandlerThread("multi-install");
thread.start();
mHandler = new Handler(thread.getLooper());
mHandler.post(this::installInternal);
IntentFilter intentFilter = new IntentFilter(MultiInstallReceiver.ACTION);
MultiInstallReceiver receiver = new MultiInstallReceiver();
// 这个receiver必需实际被注册了,才能收到通知消息。
mContext.registerReceiver(receiver, intentFilter);
}
private int mSessionId;
private void installInternal() {
File dirFile = new File("/sdcard/Download/Flipboard/");
if (!dirFile.exists()) {
Log.e(TAG, "return by not exist");
return;
}
if (!dirFile.isDirectory()) {
Log.e(TAG, "return by not dir");
return;
}
PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
params.setInstallReason(PackageManager.INSTALL_REASON_USER);
PackageInstaller.Session session = null;
try {
mSessionId = mPackageInstaller.createSession(params);
session = mPackageInstaller.openSession(mSessionId);
/**
* 方法就是只往一个session的stream中写文件。
* 写完一个文件后,执行PackageInstaller.Session的fsync方法。
*/
for (File singleFile : dirFile.listFiles()) {
FileInputStream inputStream = new FileInputStream(singleFile);
OutputStream outputStream = session.openWrite(singleFile.getName(), 0, singleFile.length());
IOUtils.copyStream(inputStream, outputStream);
session.fsync(outputStream);
}
// 提交安装请求
Intent intent = new Intent(MultiInstallReceiver.ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 1, intent, PendingIntent.FLAG_IMMUTABLE);
session.commit(pendingIntent.getIntentSender());
} catch (Exception e) {
Log.e(TAG, "install error", e);
if (session != null) session.abandon();
} finally {
if (session != null) session.close();
}
}
}
// ------------------------- IO工具类 -------------------------
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class IOUtils {
private static final String TAG = "IOUtils";
public static void copyStream(InputStream from, OutputStream to) throws IOException {
byte[] buf = new byte[1024 * 1024];
int len;
while ((len = from.read(buf)) > 0) {
to.write(buf, 0, len);
}
}
}
研究上面方案的过程中走了一些弯路,这其中遇到的接口也是有必要研究一下的:
1)PackageInstaller.SessionParams.setMultiPackage()
2)PackageInstaller.Session.addChildSessionId(int sessionId)
只在 frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java 这里看到过使用。这里记录一下,后面研究。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架