由于工作需求,近来要研究下apk加固。昨天在网上找了个加固代码,一顿操作下来,加固ok,在Android5和Android10上的机器上跑起来ok,到了Android11上安装出错。
错误信息如下:
adb: failed to install Xxxx.apk: Failure [-124: Failed parse during installPackageLI: Targeting R+ (version 30 and above) requires the resources.arsc of installed APKs to be stored uncompressed and aligned on a 4-byte boundary]
?看来是android11的行为变更,官方说明:
如果以 Android?11(API 级别?30)或更高版本为目标平台的应用包含压缩的?resources.arsc ?文件或者如果此文件未按 4 字节边界对齐,应用将无法安装。如果存在其中任意一种情况,系统将无法对此文件进行内存映射。无法进行内存映射的资源表必须读入 RAM 中的缓冲区,从而给系统造成不必要的内存压力,并大大增加设备的 RAM 使用量。
原来在Android11上为了减少ram压力,需要resources.arsc不要压缩,并且4字节对齐。
本以为还要看源码鼓捣下,没想到文档已经说得很清楚了,还是要多看文档啊!接下来就简单了,按照以下方式打包签名即可:
1.打包APK时,不要对resources.arsc进行压缩
2.将上一步打包好的apk做V1签名
3.将V1签名的apk做zipalign四字节对齐
4.对齐后的apk进行V2签名
上代码,需要的自取,记得把zipalign、apksigner(SDK下的build-tools/30.0.2)添加到环境变量。
// 打包
System.out.println("打包APK");
File unsignedApk = new File("output/unsigned.apk");
ZipUtil.zip(apkUnzipDir, unsignedApk);
// 删除解压目录
FileUtils.delete("output/unzip/");
// 签名
System.out.println("签名APK");
File signedApk = new File("output/signed.apk");
SignUtils.apkSignature(unsignedApk, signedApk, "keystore/test.jks", "test123", "test", "test123");
System.out.println("Finished!!!");
public class ZipUtil {
public static void unZip(File zip, File dir) {
try {
dir.delete();
ZipFile zipFile = new ZipFile(zip);
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry zipEntry = entries.nextElement();
String name = zipEntry.getName();
if (name.equals("META-INF/CERT.RSA") || name.equals("META-INF/CERT.SF")
|| name.equals("META-INF/MANIFEST.MF")) {
continue;
}
if (!zipEntry.isDirectory()) {
File file = new File(dir, name);
if (!file.getParentFile().exists())
file.getParentFile().mkdirs();
FileOutputStream fos = new FileOutputStream(file);
InputStream is = zipFile.getInputStream(zipEntry);
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
is.close();
fos.close();
}
}
zipFile.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 打包apk
* @param dir apk解压后的文件夹路径
* @param zip 输出打包后的apk
* @throws Exception
*/
public static void zip(File dir, File zip) throws Exception {
zip.delete();
CheckedOutputStream cos = new CheckedOutputStream(new FileOutputStream(zip), new CRC32());
ZipOutputStream zos = new ZipOutputStream(cos);
compress(dir, zos, "");
zos.flush();
zos.close();
}
private static void compress(File srcFile, ZipOutputStream zos, String basePath) throws Exception {
if (srcFile.isDirectory()) {
compressDir(srcFile, zos, basePath);
} else {
compressFile(srcFile, zos, basePath);
}
}
private static void compressDir(File dir, ZipOutputStream zos, String basePath) throws Exception {
File[] files = dir.listFiles();
if (files.length < 1) {
ZipEntry entry = new ZipEntry(basePath + dir.getName() + "/");
zos.putNextEntry(entry);
zos.closeEntry();
}
for (File file : files) {
compress(file, zos, basePath + dir.getName() + "/");
}
}
private static void compressFile(File file, ZipOutputStream zos, String dir) throws Exception {
String dirName = dir + file.getName();
String[] dirNameNew = dirName.split("/");
StringBuffer buffer = new StringBuffer();
if (dirNameNew.length > 1) {
for (int i = 1; i < dirNameNew.length; i++) {
buffer.append("/");
buffer.append(dirNameNew[i]);
}
} else {
buffer.append("/");
}
ZipEntry entry = new ZipEntry(buffer.toString().substring(1));
if ("resources.arsc".equals(file.getName())) {
entry.setMethod(ZipEntry.STORED);
entry.setSize(file.length());
entry.setCrc(calFileCRC32(file));
}
zos.putNextEntry(entry);
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
int count;
byte data[] = new byte[1024];
while ((count = bis.read(data, 0, 1024)) != -1) {
zos.write(data, 0, count);
}
bis.close();
zos.closeEntry();
}
private static long calFileCRC32(File file) throws IOException {
FileInputStream fi = new FileInputStream(file);
CheckedInputStream checksum = new CheckedInputStream(fi, new CRC32());
while (checksum.read() != -1) { }
long temp = checksum.getChecksum().getValue();
fi.close();
checksum.close();
return temp;
}
}
public class SignUtils {
private SignUtils() {
throw new UnsupportedOperationException("u can't instantiate me...");
}
private static void exec(String[] cmd, String execName) throws IOException, InterruptedException {
Process process = Runtime.getRuntime().exec(cmd);
System.out.println("start " + execName);
try {
process.waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
throw e;
}
if (process.exitValue() != 0) {
InputStream inputStream = process.getErrorStream();
int len;
byte[] buffer = new byte[2048];
ByteArrayOutputStream bos = new ByteArrayOutputStream();
while ((len = inputStream.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
System.out.println(new String(bos.toByteArray(), "gbk"));
throw new RuntimeException(execName + " execute fail");
}
System.out.println("finish " + execName);
process.destroy();
}
/**
* V1签名
*/
private static String signature(File unsignedApk, String keyStore, String keyPwd, String alias, String alisaPwd)
throws InterruptedException, IOException {
String path = unsignedApk.getAbsolutePath();
String v1Name = path.substring(0, path.indexOf(".apk")) + "_v1.apk";
String cmd[] = { "cmd.exe", "/C ", "jarsigner", "-sigalg", "SHA1withRSA", "-digestalg", "SHA1", "-keystore",
keyStore, "-storepass", keyPwd, "-keypass", alisaPwd, "-signedjar", v1Name,
unsignedApk.getAbsolutePath(), alias };
exec(cmd, "v1 sign");
FileUtils.delete(path);
return v1Name;
}
// zipalign -p 4 input\app-release-unsigned.apk input\app-release-unsigned.apk
private static String apkZipalign(String v1Apk) throws IOException, InterruptedException {
String zipalignName = v1Apk.substring(0, v1Apk.indexOf(".apk")) + "_align.apk";
String cmd[] = {"cmd.exe", "/C ", "zipalign", "-p", "4", v1Apk, zipalignName};
exec(cmd, "zipalign");
FileUtils.delete(v1Apk);
return zipalignName;
}
//apksigner.jar sign --ks key.jks --ks-key-alias releasekey --ks-pass pass:pp123456 --key-pass pass:pp123456 --out output.apk input.apk
public static void apkSignature(File unsignedApk, File signedApk, String keyStore, String keyPwd, String alias, String alisaPwd) throws IOException, InterruptedException {
String v1Name = signature(unsignedApk, keyStore, keyPwd, alias, alisaPwd);
String zipalignName = apkZipalign(v1Name);
String cmd[] = { "cmd.exe", "/C ", "apksigner", "sign", "--ks", keyStore, "--ks-pass", "pass:" + keyPwd,
"--ks-key-alias", alias, "--key-pass", "pass:" + alisaPwd,
"--out", signedApk.getAbsolutePath(), zipalignName };
exec(cmd, "v2 sign");
FileUtils.delete(zipalignName);
FileUtils.delete(signedApk.getAbsolutePath() + ".idsig");
}
}
|