朋友谈天讨论到一个问题,怎样检测zip的完整性。zip是咱们常用的紧缩格局,不管是Win/Mac/Linux下都很常用,咱们做文件的下载也会经常用到,网络充满不确定性,关于多个小文件(比方配置文件)的下载,咱们希望只发起一次衔接,因为建立衔接是很耗费资源的,即便现在http2.0能够对一条TCP衔接进行复用,咱们仍是希望网络恳求的次数越少越好,不管是关于稳定性仍是成功失利的逻辑判别,都会有好处。

这个时分咱们常用的其实便是把他们紧缩成一个zip文件,下载下来之后解压就好了。

但很多时分zip会解压失利,如果咱们的zip现已下载下来了,其实不存在没有访问权限的问题了,那原因除了空间不行之外,便是zip格局有问题了,zip文件为空或许只下载了一半。
这个时分就需求检测一下咱们下载下来的zip是不是合法有效的zip了。
有这么几种思路:

  1. 直接解压,抛反常标明zip有问题
  2. 下载前得到zip文件的length,下载后检测文件巨细
  3. 运用md5或sha1等摘要算法,下载下来后做md5,然后比对合法性
  4. 检测zip文件完毕的特殊编码格局,检测是否zip合法

这几种做法有利有弊,这儿咱们只看第4种。
咱们讨论之前,能够大致了解一下zip的格局ZIP文件格局剖析,咱们重视的是End of central directory record,中心目录完毕符号,每个zip只会呈现一次。

Offset Bytes Description
0 4 End of central directory signature = 0x06054b50 中心目录完毕符号(0x06054b50)
4 2 Number of this disk 当时磁盘编号
6 2 number of the disk with the start of the central directory 中心目录开端方位的磁盘编号
8 2 total number of entries in the central directory on this disk 该磁盘上所记录的中心目录数量
10 2 total number of entries in the central directory 中心目录结构总数
12 4 Size of central directory (bytes) 中心目录的巨细
16 4 offset of start of central directory with respect to the starting disk number 中心目录开端方位相关于archive开端的位移
20 2 .ZIP file comment length(n) 注释长度
22 n .ZIP Comment 注释内容

咱们能够看到,0x06054b50地点的方位其实是在zip.length减去22个字节,所以咱们只需求seek到需求的方位,然后读4个字节看是否是0x06054b50,就能够确定zip是否完整。
下面是一个判别的代码

        //没有zip文件注释时分的目录完毕符的偏移量
        private static final int RawEndOffset = 22;
        //0x06054b50占4个字节
        private static final int endOfDirLength = 4;
        //目录完毕标识0x06054b50 的小端读取方法。
        private static final byte[] endOfDir = new byte[]{0x50, 0x4B, 0x05, 0x06};
        private boolean isZipFile(File file) throws IOException {
            if (file.exists() && file.isFile()) {
                if (file.length() <= RawEndOffset + endOfDirLength) {
                    return false;
                }
                long fileLength = file.length();
                RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
                //seek到完毕符号地点的方位
                randomAccessFile.seek(fileLength - RawEndOffset);
                byte[] end = new byte[endOfDirLength];
                //读取4个字节
                randomAccessFile.read(end);
                //关掉文件
                randomAccessFile.close();
                return isEndOfDir(end);
            } else {
                return false;
            }
        }
        /**
         * 是否契合文件夹完毕符号
         */
        private boolean isEndOfDir(byte[] src) {
            if (src.length != endOfDirLength) {
                return false;
            }
            for (int i = 0; i < src.length; i++) {
                if (src[i] != endOfDir[i]) {
                    return false;
                }
            }
            return true;
        }

有人可能留意到了,你上面写的完毕标识分明是0x06054b50,为什么检测的时分是反着写的。这儿就涉及到一个大端小端的问题,录音的时分也能会遇到巨细端顺序的问题,反过来读就好了。

涉及到二进制的检查和修正,咱们能够运用010editor这个软件来检查文件的十六进制或许二进制,并且能够手动修正某个方位的二进制。

检测zip文件完整(进阶:APK文件渠道号)

他的界面大致长这样子,小端显示的,咱们能够看到咱们要得到的06 05 4b 50

咱们看上面的表格里面最终一个表格里的 .ZIP file comment length(n).ZIP Comment ,意思是描绘长度是两个字节,描绘长度是n,表示这个长度是可变的。这个有啥效果呢?
其实便是给了一个能够写额定的描绘数据的当地(.ZIP Comment),他的长度由前面的.ZIP file comment length(n)来操控。也便是zip答应你在它的文件完毕后面额定的追加内容,而不会影响前面的数据。描绘文件的长度是两个字节,也便是一个short的长度,所以理论上能够寻址2^16^个方位。
举个比如:
修正之前:

检测zip文件完整(进阶:APK文件渠道号)

修正之后

检测zip文件完整(进阶:APK文件渠道号)

看上面两个文件,修正之前长度为0,咱们把它改成2(留意巨细端),咱们改成2,然后随意在后面追加两个byte,保存,翻开修正之后的zip,发现是能够正常运转的,甚至咱们能够在长度是2的基础上追加多个byte,其实仍是能够翻开的。
所以回到标题内容,其实apk便是zip,咱们同样能够在apk的Comment后面追加内容,比方能够作为途径来源,或许完成这样的需求:h5网页A上下载的需求翻开某个ActivityA,h5网页B上下载的需求翻开某个ActivityB。

原理仍是上面的原理,写入途径或许配置,读取apk途径或许配置,做相应统计或许操作。

        //magic -> yocn
        private static final byte[] MAGIC = new byte[]{0x79, 0x6F, 0x63, 0x6E};
        //没有zip文件注释时分的目录完毕符的偏移量
        private static final int RawEndOffset = 22;
        //0x06054b50占4个字节
        private static final int endOfDirLength = 4;
        //目录完毕标识0x06054b50 的小端读取方法。
        private static final byte[] endOfDir = new byte[]{0x50, 0x4B, 0x05, 0x06};
        //注释长度占两个字节,所以理论上能够支持 2^16 个字节。
        private static final int commentLengthBytes = 2;
        //注释长度
        private static final int commentLength = 8;
        private boolean isZipFile(File file) throws IOException {
            if (file.exists() && file.isFile()) {
                if (file.length() <= RawEndOffset + endOfDirLength) {
                    return false;
                }
                long fileLength = file.length();
                RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
                //seek到完毕符号地点的方位
                randomAccessFile.seek(fileLength - RawEndOffset);
                byte[] end = new byte[endOfDirLength];
                //读取4个字节
                randomAccessFile.read(end);
                //关掉文件
                randomAccessFile.close();
                return isEndOfDir(end);
            } else {
                return false;
            }
        }
        /**
         * 是否契合文件夹完毕符号
         */
        private boolean isEndOfDir(byte[] src) {
            if (src.length != endOfDirLength) {
                return false;
            }
            for (int i = 0; i < src.length; i++) {
                if (src[i] != endOfDir[i]) {
                    return false;
                }
            }
            return true;
        }
        /**
         * zip(apk)尾追加途径信息
         */
        private void write2Zip(File file, String channelInfo) throws IOException {
            if (isZipFile(file)) {
                long fileLength = file.length();
                RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
                //seek到完毕符号地点的方位
                randomAccessFile.seek(fileLength - commentLengthBytes);
                byte[] lengthBytes = new byte[2];
                lengthBytes[0] = commentLength;
                lengthBytes[1] = 0;
                randomAccessFile.write(lengthBytes);
                randomAccessFile.write(getChannel(channelInfo));
                randomAccessFile.close();
            }
        }
        /**
         * 获取zip(apk)文件完毕
         *
         * @param file 目标哦文件
         */
        private String getZipTail(File file) throws IOException {
            long fileLength = file.length();
            RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
            //seek到magic的方位
            randomAccessFile.seek(fileLength - MAGIC.length);
            byte[] magicBytes = new byte[MAGIC.length];
            //读取magic
            randomAccessFile.read(magicBytes);
            //如果不是magic完毕,返回空
            if (!isMagicEnd(magicBytes)) return "";
            //seek到读到信息的offest
            randomAccessFile.seek(fileLength - commentLength);
            byte[] lengthBytes = new byte[commentLength];
            //读取途径
            randomAccessFile.read(lengthBytes);
            randomAccessFile.close();
            char[] lengthChars = new char[commentLength];
            for (int i = 0; i < commentLength; i++) {
                lengthChars[i] = (char) lengthBytes[i];
            }
            return String.valueOf(lengthChars);
        }
        /**
         * 是否以魔数完毕
         *
         * @param end 检测的byte数组
         * @return 是否完毕
         */
        private boolean isMagicEnd(byte[] end) {
            for (int i = 0; i < end.length; i++) {
                if (MAGIC[i] != end[i]) {
                    return false;
                }
            }
            return true;
        }
        /**
         * 生成途径byte数组
         */
        private byte[] getChannel(String s) {
            byte[] src = s.getBytes();
            byte[] channelBytes = new byte[commentLength];
            System.arraycopy(src, 0, channelBytes, 0, commentLength);
            return channelBytes;
        }
      //读取源apk的路径
      public static String getSourceApkPath(Context context, String packageName) {
        if (TextUtils.isEmpty(packageName))
            return null;
        try {
            ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(packageName, 0);
                return appInfo.sourceDir;
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            }
        return null;
    }

这儿运用了一个魔数的概念,标明是否是写入了咱们特定的途径,只要写了咱们特定途径的基础上才会去读取,避免读到了没有写过的文件。
读取途径的时分首要获取装置包的绝对路径。Android体系在用户装置app时,会把用户装置的apk拷贝一份到/data/apk/路径下,通过getSourceApkPath 能够获取该apk的绝对路径。如果运用rw可能会有权限问题,所以读取的时分只运用r就能够了。

参阅: ZIP文件格局剖析 全民K歌增量升级计划