apk压缩包的大小通过优化以后变小html
对apk中的图片进行压缩java
【1】svg图片:文件是对图片的描述,牺牲CPU的计算能力的,节省空间。linux
【2】webp图片:在图片压缩的时候能够指定压缩的格式为webp。在android中是支持webp图片显示的。android
使用的原则:简单的图标。git
图片格式的大小的比较:png>jpeg>webpgithub
webp是由VP8派生而来的。webp的无损压缩比PNG文件小45%左右,即便PNG进过其余的压缩工具压缩后,能够减少到PNG的28%。web
使用的厂商:Facebook在用、腾讯、淘宝数组
缺点:加载相比于PNG要慢不少,随着如今手机配置变高,可使用。服务器
转换的工具:http://isparta.github.io/微信
常见:emoji表情、皮肤、动态下载资源、模块的插件化动态加载,例如small等工具。
Lint工具会提供一写优化建议的点,能够采用,可是并不是全部的建议都是正确的。
目的也是同样的,就是为了减少apk的大小
1)检测没有用的布局 删除
2)未使用到的资源 好比 图片 ---删除
3)建议String.xml有一些没有用到的字符。
7zZip工具的使用
1)能够删除注释和不用的代码。
2)将java文件名改为短名a.java,b.java
3)方法名变为无心义的字符
资源打包的过程:
【参考】https://www.jianshu.com/p/3cc131db2002,下面的内容摘自连接文章
开发app时,须要代码和资源。最终生成的apk中代码转换为了dex文件,那么apk文件中的资源是否仍是app开发时那些资源文件呢?或者说这些资源文件是否发生了什么变化?
引用老罗一张关于资源打包过程以及查找的图:
从上图能够看出:
除了assets和res/raw资源被原装不动地打包进APK以外,其它的资源都会被编译或者处理.xml文件会被编译为二进制的xml,因此解压apk后,没法直接打开xml文件。
除了assets资源以外,其它的资源都会被赋予一个资源ID。
打包工具负责编译和打包资源,编译完成以后,会生成一个resources.arsc文件和一个R.java,前者保存的是一个资源索引表,后者定义了各个资源ID常量,供在代码中索引资源。
应用程序配置文件AndroidManifest.xml一样会被编译成二进制的XML文件,而后再打包到APK里面去。
应用程序在运行时最终是经过AssetManager来访问资源,或经过资源ID来访问,或经过文件名来访问。
在生成的apk中,只有assets和res/raw资源被原装不动地打包进apk。其它的资源都会被编译或者处理。可使用以下命令查看apk中的文件列表:
aapt l -v apkfile
将apk直接解压后,会发现xml都打不开,提示格式不对,由于其已经变为二进制xml了。另外PNG等图片也会进行相应的优化。还有就是多了一个resources.arsc文件。
在android中存在与对应R文件描述的一个二进制的资源文件,名称为:resources.arsc
参考资源:https://bbs.pediy.com/thread-207816.htm
Android 中具备18个资源维度,
【原理】再次作混淆:将长名称改成短名字命名;
实际的替换内容:将资源文件的名称改成名字,而后将映射文件resources.arsc的中的字符串修改成对应的短名字
再作“混淆”:要实现将res/drawable/ic_launcher.png图片改为a.png
drawable文件的名字
String文件的名字
layout的名字
好比:R.string.description--->R.string.a
res/drawable/ic_launcher.png图片改为a.png
还能够更加夸张
res/drawable--->r/d
res/value-->r/v
res/drawable/ic_launcher.png图片改为r/d/a.png
依据:根据二进制文件中提供的信息,从新再造一张表(一个二进制文件),区别就是将旧表的中的字符串信息替换为简单的名称,减少占用的空间
【LEDataInputStream.java】工具类
1 public final class LEDataInputStream 2 implements DataInput { 3 private static final String EMBEDDED_COPYRIGHT = "copyright (c) 1999-2010 Roedy Green, " + 4 "Canadian Mind Products, http://mindprod.com"; 5 protected final DataInputStream dis; 6 protected final InputStream is; 7 protected final byte[] work; 8 9 public static String readUTF(DataInput in) 10 throws IOException { 11 return DataInputStream.readUTF(in); 12 } 13 14 public LEDataInputStream(InputStream in) { 15 this.is = in; 16 this.dis = new DataInputStream(in); 17 this.work = new byte[8]; 18 } 19 20 public final void close() 21 throws IOException { 22 this.dis.close(); 23 } 24 25 public final int read(byte[] ba, int off, int len) 26 throws IOException { 27 return this.is.read(ba, off, len); 28 } 29 30 public final boolean readBoolean() 31 throws IOException { 32 return this.dis.readBoolean(); 33 } 34 35 public final byte readByte() 36 throws IOException { 37 return this.dis.readByte(); 38 } 39 40 public final char readChar() //读取一个字节 41 throws IOException { 42 this.dis.readFully(this.work, 0, 2); 43 return (char) ((this.work[1] & 0xFF) << 8 | this.work[0] & 0xFF); 44 } 45 46 public final double readDouble() 47 throws IOException { 48 return Double.longBitsToDouble(readLong()); 49 } 50 51 public final float readFloat() 52 throws IOException { 53 return Float.intBitsToFloat(readInt()); 54 } 55 56 public final void readFully(byte[] ba) 57 throws IOException { 58 this.dis.readFully(ba, 0, ba.length); 59 } 60 61 public final void readFully(byte[] ba, int off, int len) 62 throws IOException { 63 this.dis.readFully(ba, off, len); 64 } 65 66 public final int readInt() //读4个字节 67 throws IOException { 68 this.dis.readFully(this.work, 0, 4); 69 return (this.work[3] << 24 | (this.work[2] & 0xFF) << 16 | (this.work[1] & 0xFF) << 8 | 70 this.work[0] & 0xFF); 71 } 72 73 @Deprecated 74 public final String readLine() 75 throws IOException { 76 return this.dis.readLine(); 77 } 78 79 public final long readLong() 80 throws IOException { 81 this.dis.readFully(this.work, 0, 8); 82 return (this.work[7] << 56 | 83 (this.work[6] & 0xFF) << 48 | 84 (this.work[5] & 0xFF) << 40 | 85 (this.work[4] & 0xFF) << 32 | 86 (this.work[3] & 0xFF) << 24 | 87 (this.work[2] & 0xFF) << 16 | 88 (this.work[1] & 0xFF) << 8 | 89 this.work[0] & 0xFF); 90 } 91 92 public final short readShort() //读取2个字节 93 throws IOException { 94 this.dis.readFully(this.work, 0, 2); 95 return (short) ((this.work[1] & 0xFF) << 8 | this.work[0] & 0xFF); 96 } 97 98 public final String readUTF() 99 throws IOException { 100 return this.dis.readUTF(); 101 } 102 103 public final int readUnsignedByte() 104 throws IOException { 105 return this.dis.readUnsignedByte(); 106 } 107 108 public final int readUnsignedShort() 109 throws IOException { 110 this.dis.readFully(this.work, 0, 2); 111 return ((this.work[1] & 0xFF) << 8 | this.work[0] & 0xFF); 112 } 113 114 public final int skipBytes(int n) 115 throws IOException { 116 return this.dis.skipBytes(n); 117 } 118 }
【demo】读取二进制表resources_arsc的简单示例
1 /** 2 * @function: 示例解析了resources_arsc二进制表的Resource Table、StringPool、packageHeader 三部份内容,没有彻底读取结束 3 * 简单的演示 4 */ 5 6 public class ArscTest { 7 public static void main(String[] args){ 8 ArscTest arscTest = new ArscTest(); 9 try { 10 // arscTest.readInputStream("C:/Users/luoding/workspace_test/arscTest/input.apk"); 11 // arscTest.readInputStream("C:/Users/luoding/workspace_test/arscTest/Lsn10SearchView.apk"); 12 arscTest.readInputStream("C:/Users/luoding/workspace_test/arscTest/dn_jobschduler.apk"); 13 } catch (IOException e) { 14 // TODO Auto-generated catch block 15 e.printStackTrace(); 16 } 17 } 18 19 @SuppressWarnings("resource") 20 public void readInputStream(String path) throws IOException{ 21 //读取压缩文件:resources_arsc 22 ZipFile zipFile = new ZipFile(path); 23 //以流形式读进来 24 InputStream inputStream = zipFile.getInputStream(new ZipEntry(""+"resources.arsc")); 25 LEDataInputStream leDataInputStream = new LEDataInputStream(inputStream); 26 //Resource Table 27 // 读整个包的RES_TABLE_TYPE,2个字节? 28 short type = leDataInputStream.readShort(); 29 //跳过了--整个包的头大小:2个字节 30 leDataInputStream.skipBytes(2); 31 //整个包的文件大小 32 leDataInputStream.readInt(); 33 //整个包的package个数,通常的个数是1个 34 int packageNum = leDataInputStream.readInt(); 35 System.out.println("num of package:"+packageNum); 36 37 //StringPool对应的内容 38 // 直接了4个字节:包含RES_STRING_POOL_TYPE+头的大小,没有使用?? 39 int got =leDataInputStream.readInt(); 40 //块大小 41 int chunkSize = leDataInputStream.readInt(); 42 //字符串数量 43 int stringCount = leDataInputStream.readInt(); 44 //style数量 45 int styleCount = leDataInputStream.readInt(); 46 //标记 47 int flags = leDataInputStream.readInt(); 48 //字符串起始位置 49 int stringsOffset = leDataInputStream.readInt(); 50 //style起始位置 51 int stylesOffset = leDataInputStream.readInt(); 52 //下面的循环读的是字符串的偏移数组,每一个元素存的是字符串的起始位置 53 int[] array = new int[stringCount]; 54 for (int i = 0; i < stringCount; ++i){ 55 array[i] = leDataInputStream.readInt(); 56 } 57 //下面读取的style的内容 58 if (styleCount != 0) { 59 for (int i = 0; i < styleCount; ++i) 60 array[i] = leDataInputStream.readInt(); 61 } 62 63 //字符串长度 64 int size = ((stylesOffset == 0) ? chunkSize : stylesOffset) - stringsOffset; 65 byte[] m_strings = new byte[size]; 66 StringBuffer ss = new StringBuffer(); 67 leDataInputStream.readFully(m_strings); 68 //读取了全部的字符串的内容 69 for(int i = 0;i<m_strings.length;i++){ 70 //(经过打开resources.arsc看到一些乱码 猜得出字符都是ASCII码) 71 char c = (char) m_strings[i]; 72 ss.append(c); 73 } 74 System.out.println(ss.toString()); 75 if (stylesOffset != 0) { 76 size = chunkSize - stylesOffset; 77 if (size % 4 != 0) 78 throw new IOException("Style data size is not multiple of 4 (" + size + ")."); 79 80 for (int i = 0; i < size / 4; ++i) 81 leDataInputStream.readInt(); 82 } 83 84 //nextChunk--packageHeader 85 leDataInputStream.readShort(); 86 leDataInputStream.skipBytes(2); 87 leDataInputStream.readInt(); 88 89 int id = (byte) leDataInputStream.readInt(); 90 StringBuilder sb = new StringBuilder(16); 91 int length = 256; 92 //拿包名 93 while (length-- != 0) { 94 short ch = leDataInputStream.readShort(); 95 if (ch == 0) 96 break; 97 sb.append((char)ch); 98 } 99 System.out.println("pacakgeName:"+sb.toString()); 100 } 101 }
1 /** 2 * 程序的总入口: 3 * @param args 4 */ 5 public static void main(String[] args) { 6 mBeginTime = System.currentTimeMillis(); 7 Main m = new Main(); 8 //获取保存的优化后文件的路径 9 getRunningLocation(m); 10 //运行程序:须要传入args参数 11 m.run(args); 12 }
【args参数的来源】
【解析args源码】是在run方法中:下面截取了部分代码
1 private void run(String[] args) { 2 if (args.length < 1) { 3 goToError(); 4 } 5 6 File configFile = null;// 配置文件 7 File outputFile = null;// 输出文件 8 String apkFileName = null;// apk输入文件 9 10 File signatureFile = null;// 签名秘钥 11 File mappingFile = null;// 修改后映射文件 12 String keypass = null;// 秘钥主密码 13 String storealias = null;// 别名 14 String storepass = null;// 别名密码 15 16 String signedFile = null;// 签名apk 17 // 检查参数 18 for (int index = 0; index < args.length; ++index) { 19 String arg = args[index]; 20 if ((arg.equals("--help")) || (arg.equals("-h"))) { 21 goToError(); 22 } else if (arg.equals("-config")) {// 配置文件不能最后读,后面的执行须要依赖配置文件,或者不是.xml则报错 23 if ((index == args.length - 1) || (!(args[(index + 1)].endsWith(".xml")))) { 24 System.err.println("Missing XML configuration file argument"); 25 goToError(); 26 } 27 // 参数后面的参数才是config配置文件的路径 28 configFile = new File(args[(++index)]); 29 if (!(configFile.exists())) { 30 System.err.println(configFile.getAbsolutePath() + " does not exist"); 31 goToError(); 32 } 33 System.out.printf("special configFile file path: %s\n", new Object[] { configFile.getAbsolutePath() });
【config.xml】其中解析的配置文件:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <resproguard> 3 <!--defaut property to set --> 4 <issue id="property"> 5 <!--是否使用极限压缩:7zip --> 6 <!--whether use 7zip to repackage the signed apk, you must install the 7z command line version in window --> 7 <!--sudo apt-get install p7zip-full in linux --> 8 <!--and you must write the sign data fist, and i found that if we use linux, we can get a better result --> 9 <seventzip value="true"/> 10 <!--密钥--> 11 <!--the sign data file name in your apk, default must be META-INF--> 12 <!--generally, you do not need to change it if you dont change the meta file name in your apk--> 13 <metaname value="META-INF"/> 14 <!--名称的极端的压缩--> 15 <!--if keep root, res/drawable will be kept, it won't be changed to such as r/s--> 16 <keeproot value="false"/> 17 </issue>
微信中的r目录下的文件 命名:
继续看config.xml的内容
1 <!--whitelist, some resource id you can not proguard, such as getIdentifier--> 2 <!--isactive, whether to use whitelist, you can set false to close it simply--> 3 <!--白名单:能够指定第三方的资源不被修更名称 --> 4 <issue id="whitelist" isactive="true"> 5 <!--you must write the full package name, such as com.tencent.mm.R --> 6 <!--for some reason, we should keep our icon better--> 7 <!--and it support *, ?, such as com.tencent.mm.R.drawable.emoji_*, com.tencent.mm.R.drawable.emoji_?--> 8 <path value="com.codeboy.autoresguarddemo.R.mipmap.ic_launcher"/> 9 </issue> 10 11 <!--是否生成映射文件 --> 12 <!--keepmapping, sometimes if we need to support incremental upgrade, we should keep the old mapping--> 13 <!--isactive, whether to use keepmapping, you can set false to close it simply--> 14 <!--if you use -mapping to set sign property in cammand line, these setting will be overlayed--> 15 <issue id="keepmapping" isactive="false"> 16 <!--the old mapping path, in window use \, in linux use /, and the default path is the running location--> 17 <path value="resource_mapping.txt"/> 18 </issue>
映射文件的内容部分截取以下:
config.xml 还包括是否使用签名,其中指定签名的路径和密码等
1 <!--sign, if you want to sign the apk, and if you want to use 7zip, you must fill in the following data--> 2 <!--isactive, whether to use sign, you can set false to close it simply--> 3 <!--if you use -signature to set sign property in cammand line, these setting will be overlayed--> 4 <issue id="sign" isactive="true"> 5 <!-- <issue id="sign" isactive="false"> --> 6 <!--the signature file path, in window use \, in linux use /, and the default path is the running location--> 7 <!-- <path value="签名路径"/> --> 8 <!-- storepass --> 9 <!-- <storepass value="签名密码"/> --> 10 <!-- keypass --> 11 <!-- <keypass value="别名密码"/> --> 12 <!-- alias --> 13 <!-- <alias value="别名"/> --> 14 <path value="C:\Users\luoding\workspace_test\dn_android_resproguard\mykeystore.keystore"/> 15 <!--storepass--> 16 <storepass value="123456"/> 17 <!--keypass--> 18 <keypass value="123456"/> 19 <!--alias--> 20 <alias value="123456"/> 21 </issue>
能够经过out参数指定输出的文件的目录
在生成的打包文件具备多种类型:
其中的zipaligne是对齐,至关于磁盘的碎片整理,具备专门的zipalign工具
参考文章:http://bbs.ihei5.com/thread-171596-1-1.html
关于Android平台的Zipalign或Zipaligned: 什么是Zipalign? Zipalign是一个档案整理工具,它首次被介绍是在Android 1.6版本的SDK(Software Development Kit)软件开发工具包中。它优化Android应用程序包(APK)到整合包, 以使Android操做系统与应用程序之间的交互做用更有效率,而后应用程序和总体系统的运行速度更快,发挥更大的潜能。它使Zipaligned的应用程序执行时间达到最低限度,其最终结果致使当设备运行APK应用程序时占更少的RAM(Random Access Memory)随机访问内存 Zipalign如何准备的执行(工做)呢? 在Android的操做环境中,存储在每一个应用程序包的数据文件经过多个进程访问,例如,程序安装工具将读取数据列表肯定相关的权限;由于包括显示通知等在内的多种缘由,系统服务器能够读取这些资源;主界面应用程序要读取资源以便获取应用程序的名称与图标等。由于Android是基于一个真正的多任务操做基础架构,这些文件是不断地读取。最后但也是最重要的,应用程序自己读取体现的数据 由于Android操做系统基于Linux架构,存储单元布置(Memory Mapping)在有效的处理过程当中起着一个关键的做用。从本质上而讲,为Android操做系统资源的处理代码最佳的整理是4字节界层。这个意思是说,若是APK应用程序包是存储单元布置到4字节界层,依据相应的整理,操做系统将不须要通读整个应用程序包以获取所须要的数据表,而每个系统处理都将提早知道到哪里去寻找它所须要的资源,所以执行效率更快(运行更平滑,速度更快) 总结而讲,Zipalign一个APK应用程序的结果便是让全部的未压缩的数据以整合包的形式被整理到4字节界层,让全部的数据能够直接从存储器映象读取。而由于正访问的代码没有通读到整个应用程序包使得执行消耗的程序运行内存(RAM)下降。最终,操做系统总体上的速度会更快 UnZipalign(未整理)的APK应用程序包有什么劣势呢? 这是很容易理解的,对于未整理的应用程序包的状况,资源读取将会缓慢,程序运行内存(RAM)的使用将会处在一个较高的范围。它也取决于当前有多少未整理的应用程序。例如,若是有着较少的应用程序,而后有一个未整理的主界面程序,在启动时间上,你能观察到更慢的应用程序,这是最理想的状况。 对于一个糟糕的状况,若有着许多的未整理的应用程序,将会致使系统反复的启动和结束进程,系统运行将会滞后,电池的使用时间将会大幅度下降 咱们应该怎么作呢? 当前,Zipalign档案整理工具已变成Android SDK(Software Development Kit)软件开发工具包的一部分。它能够在电脑上已安装好的Android SDK “tools” 或“platform-tools” 目录或文件夹中被查看 对于使用Zipalign档案整理工具,能够简单地输入并执行Command命令—— zipalign [-f] [-v] <alignment> infile.apk outfile.apk 其中,infile.apk是源文件,outfile.apk是输出文件 更多的,咱们也能够确认APK应用程序的整理,输入并执行Command命令—— zipalign -c -v <alignment> existing.apk 其中,existing.apk能被任何你须要的应用程序包得到确认。 另外<alignment>命令须要是一个整数值,否则,命令会返回无效的错误提示。而这个值,虽然能够是任何的整数,但必须始终是4,由于这将提供32位元的整理层,任何其余的值,它们都是有效的,但不起任何做用 对于这些Command命令的标识—— -f—重写存在的outfile.zip -v—提供详细的输出 -c—确认一个给定的文件的整理 有什么要注意呢? Zipalign操做必须且仅在标记APK文件附有我的加密钥以后。若是在标记以前进行Zipalign操做,标记过程将会干扰整理
其中还存在压缩选项:主要是对图片的压缩
1 <!--compress, if you want to compress the file, the name is relative path, such as resources.arsc, res/drawable-hdpi/welcome.png--> 2 <!--what can you compress? generally, if your resources.arsc less than 1m, you can compress it. and i think compress .png, .jpg is ok--> 3 <!--isactive, whether to use compress, you can set false to close it simply--> 4 <issue id="compress" isactive="true"> 5 <!--you must use / separation, and it support *, ?, such as *.png, *.jpg, res/drawable-hdpi/welcome_?.png--> 6 <path value="*.png"/> 7 <path value="*.jpg"/> 8 <path value="*.jpeg"/> 9 <path value="*.gif"/> 10 <path value="resources.arsc"/> 11 </issue>
【完整的配置文件】config.xml
1 <?xml version="1.0" encoding="UTF-8"?> 2 <resproguard> 3 <!--defaut property to set --> 4 <issue id="property"> 5 <!--是否使用极限压缩:7zip --> 6 <!--whether use 7zip to repackage the signed apk, you must install the 7z command line version in window --> 7 <!--sudo apt-get install p7zip-full in linux --> 8 <!--and you must write the sign data fist, and i found that if we use linux, we can get a better result --> 9 <seventzip value="true"/> 10 <!--密钥--> 11 <!--the sign data file name in your apk, default must be META-INF--> 12 <!--generally, you do not need to change it if you dont change the meta file name in your apk--> 13 <metaname value="META-INF"/> 14 <!--名称的极端的压缩--> 15 <!--if keep root, res/drawable will be kept, it won't be changed to such as r/s--> 16 <keeproot value="false"/> 17 </issue> 18 19 <!--whitelist, some resource id you can not proguard, such as getIdentifier--> 20 <!--isactive, whether to use whitelist, you can set false to close it simply--> 21 <!--白名单:能够指定第三方的资源不被修更名称 --> 22 <issue id="whitelist" isactive="true"> 23 <!--you must write the full package name, such as com.tencent.mm.R --> 24 <!--for some reason, we should keep our icon better--> 25 <!--and it support *, ?, such as com.tencent.mm.R.drawable.emoji_*, com.tencent.mm.R.drawable.emoji_?--> 26 <path value="com.codeboy.autoresguarddemo.R.mipmap.ic_launcher"/> 27 </issue> 28 29 <!--是否生成映射文件 --> 30 <!--keepmapping, sometimes if we need to support incremental upgrade, we should keep the old mapping--> 31 <!--isactive, whether to use keepmapping, you can set false to close it simply--> 32 <!--if you use -mapping to set sign property in cammand line, these setting will be overlayed--> 33 <issue id="keepmapping" isactive="false"> 34 <!--the old mapping path, in window use \, in linux use /, and the default path is the running location--> 35 <path value="resource_mapping.txt"/> 36 </issue> 37 38 <!--compress, if you want to compress the file, the name is relative path, such as resources.arsc, res/drawable-hdpi/welcome.png--> 39 <!--what can you compress? generally, if your resources.arsc less than 1m, you can compress it. and i think compress .png, .jpg is ok--> 40 <!--isactive, whether to use compress, you can set false to close it simply--> 41 <issue id="compress" isactive="true"> 42 <!--you must use / separation, and it support *, ?, such as *.png, *.jpg, res/drawable-hdpi/welcome_?.png--> 43 <path value="*.png"/> 44 <path value="*.jpg"/> 45 <path value="*.jpeg"/> 46 <path value="*.gif"/> 47 <path value="resources.arsc"/> 48 </issue> 49 50 <!--sign, if you want to sign the apk, and if you want to use 7zip, you must fill in the following data--> 51 <!--isactive, whether to use sign, you can set false to close it simply--> 52 <!--if you use -signature to set sign property in cammand line, these setting will be overlayed--> 53 <issue id="sign" isactive="true"> 54 <!-- <issue id="sign" isactive="false"> --> 55 <!--the signature file path, in window use \, in linux use /, and the default path is the running location--> 56 <!-- <path value="签名路径"/> --> 57 <!-- storepass --> 58 <!-- <storepass value="签名密码"/> --> 59 <!-- keypass --> 60 <!-- <keypass value="别名密码"/> --> 61 <!-- alias --> 62 <!-- <alias value="别名"/> --> 63 <path value="C:\Users\luoding\workspace_test\dn_android_resproguard\mykeystore.keystore"/> 64 <!--storepass--> 65 <storepass value="123456"/> 66 <!--keypass--> 67 <keypass value="123456"/> 68 <!--alias--> 69 <alias value="123456"/> 70 </issue> 71 72 </resproguard>
源码下载地址:https://download.csdn.net/download/wsxingjun/10584278