Java apk动态生成方案(7zipJBinding)

Java apk动态生成方案(7zipJBinding)

 次点击
18 分钟阅读

最近的项目有一个需求:动态替换一个apk中的文件,然后进行签名。

实现功能

因为我们知道,apk其实就是zip压缩包,因此我直接使用Java的ZipInputStream和ZipOutputStream修改,然后输出后进行签名不就可以了吗?

事实也的确如此,但是我踩的第一个坑就是:apk中对某些文件的对齐方式有严格要求,以及部分文件需要标记为未压缩

这个我一开始尝试手动处理,但似乎复杂度非常高,遂放弃,使用官方工具 https://developer.android.com/tools/zipalign 它会帮我们自动处理好所有的特殊格式要求

随后再使用apksigner就可以了

性能优化

然而,我需要修改的apk是一个unity游戏,里面的各类资源文件等等数量非常多,使用java自带的zip处理类性能非常差劲

随后我想到了使用7zip,经过搜索没想到还真的有java的binding库,以下是使用7zipJBinding库的代码:

package top.skidder.backend.service;

import java.io.Closeable;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import net.sf.sevenzipjbinding.*;
import net.sf.sevenzipjbinding.impl.OutItemFactory;
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;
import net.sf.sevenzipjbinding.impl.RandomAccessFileOutStream;
import net.sf.sevenzipjbinding.util.ByteArrayStream;

public class ApkBuildUtil {
    public static void buildUnsignedApk(Path baseApk, Path outputApk, String gameId) throws IOException {
        if (outputApk.getParent() != null && !Files.exists(outputApk.getParent())) {
            Files.createDirectories(outputApk.getParent());
        }
        initSevenZip();
        RandomAccessFile inRaf = null;
        RandomAccessFile outRaf = null;
        IInArchive inArchive = null;
        IOutUpdateArchive<IOutItemAllFormats> outArchive = null;
        List<Closeable> closeables = new ArrayList<>();
        try {
            inRaf = new RandomAccessFile(baseApk.toFile(), "r");
            closeables.add(inRaf);
            IInStream inStream = new RandomAccessFileInStream(inRaf);
            inArchive = SevenZip.openInArchive(null, inStream);
            closeables.add(inArchive);
            outRaf = new RandomAccessFile(outputApk.toFile(), "rw");
            outRaf.setLength(0);
            closeables.add(outRaf);
            outArchive = inArchive.getConnectedOutArchive();
            int itemCount = inArchive.getNumberOfItems();
            int sdkIndex = -1;
            for (int i = 0; i < itemCount; i++) {
                String path = (String) inArchive.getProperty(i, PropID.PATH);
                if (path != null) {
                    String norm = path.replace('\\', '/');
                    if ("assets/sdk.data".equals(norm)) {
                        sdkIndex = i;
                        break;
                    }
                }
            }
            final int finalSdkIndex = sdkIndex;
            final byte[] sdkBytes = gameId.getBytes(java.nio.charset.StandardCharsets.UTF_8);
            IOutCreateCallback<IOutItemAllFormats> callback = new IOutCreateCallback<>() {
                @Override
                public void setOperationResult(boolean operationResultOk) throws SevenZipException { }
                @Override
                public void setTotal(long total) throws SevenZipException { }
                @Override
                public void setCompleted(long complete) throws SevenZipException { }
                @Override
                public IOutItemAllFormats getItemInformation(int index, OutItemFactory<IOutItemAllFormats> outItemFactory) throws SevenZipException {
                    if (finalSdkIndex >= 0) {
                        if (index != finalSdkIndex) {
                            return outItemFactory.createOutItem(index);
                        }
                        IOutItemAllFormats item = outItemFactory.createOutItemAndCloneProperties(index);
                        item.setUpdateIsNewData(true);
                        item.setDataSize((long) sdkBytes.length);
                        return item;
                    } else {
                        if (index < itemCount) {
                            return outItemFactory.createOutItem(index);
                        } else {
                            IOutItemAllFormats item = outItemFactory.createOutItem();
                            item.setUpdateIsNewProperties(true);
                            item.setPropertyPath("assets/sdk.data");
                            item.setUpdateIsNewData(true);
                            item.setDataSize((long) sdkBytes.length);
                            return item;
                        }
                    }
                }
                @Override
                public ISequentialInStream getStream(int i) throws SevenZipException {
                    if (finalSdkIndex >= 0) {
                        return (i == finalSdkIndex) ? new ByteArrayStream(sdkBytes, true) : null;
                    } else {
                        return (i == itemCount) ? new ByteArrayStream(sdkBytes, true) : null;
                    }
                }
            };
            int outItems = (sdkIndex >= 0) ? itemCount : (itemCount + 1);
            outArchive.updateItems(new RandomAccessFileOutStream(outRaf), outItems, callback);
        } catch (SevenZipException e) {
            throw new IOException("SevenZip error: " + e.getMessage(), e);
        } catch (Exception e) {
            throw new IOException("Update APK failed: " + e.getMessage(), e);
        } finally {
            for (int i = closeables.size() - 1; i >= 0; i--) {
                try { closeables.get(i).close(); } catch (Throwable ignored) {}
            }
        }
    }
    
    public static void deleteRecursively(Path path) throws IOException {
        if (!Files.exists(path)) return;
        Files.walk(path)
                .sorted((a, b) -> b.getNameCount() - a.getNameCount())
                .forEach(p -> {
                    try {
                        Files.deleteIfExists(p);
                    } catch (IOException ignored) {}
                });
    }
    
    private static volatile boolean sevenZipInited = false;
    private static synchronized void initSevenZip() throws IOException {
        if (!sevenZipInited) {
            try {
                SevenZip.initSevenZipFromPlatformJAR();
                sevenZipInited = true;
            } catch (SevenZipNativeInitializationException e) {
                throw new IOException("Failed to init SevenZip: " + e.getMessage(), e);
            }
        }
    }
}

使用这个库后,生成一个200M大小的apk包在我的2核心服务器上耗时仅有2s左右,非常快

包名修改

这里仅做演示。只修改AndroidManifest里的包名,可以确保共存,但对于某些Apk来说可能有兼容性问题

apk里面的 AndroidManifest.xml 是经过编译的,因此当然不能直接修改

简单方式

最简单的方式是使用apktool

apktool d test.apk -o out

然后修改AndroidManifest.xml

再用apktool打包回去

优化

我们知道,AndroidManifest其实和class文件的结构有点像,它内部也有一个字符串池(String Pool)

既然我们只需要修改一些包名,那么是不是我们也可以直接修改字符串池呢?

答案是当然可以,只不过我目前没有找到现有的工具,所以需要我们自己实现。

这个工具的代码和解析我会在另外的文章中介绍

© 本文著作权归作者所有,未经许可不得转载使用。