咱们都知道apk是由:html
AS 提供了 Analyze APK 功能。java
旁边的“对比”按钮提供了diff的功能,让你能够方便的进行apk优化先后的对比。react
assets目录能够存放一些配置文件或资源文件,好比webview的本地html,react native的jsbundle等,微信的整个assets占用了13.4M。若是你的应用对本地资源要求不多的话,这个文件应该不会太大。android
lib目录下会有各类so文件,分析器会检查出项目本身的so和各类库的so。微博和微信同样只支持了arm一个平台,淘宝支持了arm和x86两个平台。git
淘宝:github
这个文件是编译后的二进制资源文件,里面是id-name-value的一个map。由于微信作了资源的混淆,因此这里能够看到资源名称都是不可读的。web
META-INF目录下存放的是签名信息,用来保证apk包的完整性和系统的安全性,帮助用户避免安装来历不明的盗版apk。算法
res目录存放的是资源文件。包括图片、字符串。raw文件夹下面是音频文件,各类xml文件等等。由于微信作了资源混淆,图片名字都不可读了。chrome
dex文件是java代码打包后的字节码,一个dex文件最多只支持65536个方法,这也是为何微信有了三个dex文件的缘由。api
由于dex分包是不均匀的,你能够理解为装箱,一个箱子的大小是固定的,但你代码的量是不肯定的,微信把前两个箱子装满了,最后还剩了2m多的代码,这些代码也占用了一个箱子,最终产生了上图不均匀的结果。
assets中会存放资源文件,这个目录中各个app存放的内容都有所不一样,因此优化也比较难。自从引入RN以来,这个目录下还会有jsbundle的信息。若是你有地址选择的功能,这里还会存放地址的映射文件(可参考全名k歌)。对于这块的资源,as是不会进行主动的删减的,因此一切都是须要靠开发者进行手动管理的。
全名k歌中的bundle文件:
中文字体是至关大的,我一直不建议将字体文件随意丢弃到assets中。有时候一个小功能急着上,开发者为了追求速度,能够先放在这里图省事。但必定要知道这个隐患,而且必定要多和产品核对功能的必要性。此外,对于有些只会用在logo中的字体,我推荐将字体文件进行删减处理。
FontZip是一个字体提取工具,readme中写到:
通过测试,已经把项目5MB的艺术字体,按需求提取后,占用只有20KB,而且可正常使用。
icon-font和svg都能完成一些icon的展现,但由于icon-font在assets中难以管理,而且功能和svg有所重叠,因此我建议减小icon-font的使用,利用svg进行代替,毕竟一个很小的icon-font也比svg大呢。我给出一个提供各类格式icon的网站,方便你们进行测试:icomoon.io/app/
字体、js代码这样的资源能动态下载的就作动态下载,虽然这样会有出错的可能性,复杂度也会提高,但这个对于app的瘦身和用户来讲是有长远的好处的。若是你用了RN,你就能够在app运行时动态去拉取最新的代码,将图片和js代码一并下载后解压使用。
有些资源文件是必需要随着app一并发布的,对于这样的文件,能够采用压缩存储的方式,在须要资源的时候将其解压使用,下面就是解压zip文件的代码:
public static void unzipFile(File zipFile, String destination) throws IOException {
FileInputStream fileStream = null;
BufferedInputStream bufferedStream = null;
ZipInputStream zipStream = null;
try {
fileStream = new FileInputStream(zipFile);
bufferedStream = new BufferedInputStream(fileStream);
zipStream = new ZipInputStream(bufferedStream);
ZipEntry entry;
File destinationFolder = new File(destination);
if (destinationFolder.exists()) {
deleteDirectory(destinationFolder);
}
destinationFolder.mkdirs();
byte[] buffer = new byte[WRITE_BUFFER_SIZE];
while ((entry = zipStream.getNextEntry()) != null) {
String fileName = entry.getName();
File file = new File(destinationFolder, fileName);
if (entry.isDirectory()) {
file.mkdirs();
} else {
File parent = file.getParentFile();
if (!parent.exists()) {
parent.mkdirs();
}
FileOutputStream fout = new FileOutputStream(file);
try {
int numBytesRead;
while ((numBytesRead = zipStream.read(buffer)) != -1) {
fout.write(buffer, 0, numBytesRead);
}
} finally {
fout.close();
}
}
long time = entry.getTime();
if (time > 0) {
file.setLastModified(time);
}
}
} finally {
try {
if (zipStream != null) {
zipStream.close();
}
if (bufferedStream != null) {
bufferedStream.close();
}
if (fileStream != null) {
fileStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
复制代码
全名k歌中的assets目录下我就发现了大量的zip文件:
android上也有一个7z库帮助咱们方便的使用7z。
一个硬件设备对应一个架构(mips、arm或者x86),只保留与设备架构相关的库文件夹(主流的架构都是arm的,mips属于小众,默认也是支持arm的so的,但x86的不支持)能够大大下降lib文件夹的大小。配置方式也十分简单,直接配置abiFilters便可:
defaultConfig {
versionCode 1
versionName '1.0.0'
renderscriptTargetApi 23
renderscriptSupportModeEnabled true
// http://stackoverflow.com/questions/30794584/exclude-jnilibs-folder-from-production-apk
ndk {
abiFilters "armeabi", "armeabi-v7a" ,"x86"
}
}
复制代码
以后生成的apk中就会排出多余的平台文件了。armeabi就不用说了,这个是必须包含的,v7是一个图形增强版本,x86是英特尔平台的支持库。
@NonNull
public static String getCpuName() {
String name = getCpuName1();
if (TextUtils.isEmpty(name)) {
name = getCpuName2();
if (TextUtils.isEmpty(name)) {
name = "unknown";
}
}
return name;
}
private static String getCpuName1() {
String[] abiArr;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
abiArr = Build.SUPPORTED_ABIS;
} else {
abiArr = new String[]{Build.CPU_ABI, Build.CPU_ABI2};
}
StringBuilder abiStr = new StringBuilder();
for (String abi : abiArr) {
abiStr.append(abi);
abiStr.append(',');
}
return abiStr.toString();
}
private static String getCpuName2() {
try {
FileReader e = new FileReader("/proc/cpuinfo");
BufferedReader br = new BufferedReader(e);
String text = br.readLine();
String[] array = text.split(":\\s+", 2);
e.close();
br.close();
return array[1];
} catch (IOException var4) {
var4.printStackTrace();
return null;
}
}
复制代码
注意:
so有个常年大坑。在Android 6.0以前,so文件会压缩到apk中。系统在安装应用的时候,会把so文件解压到data分区,这样同一个so文件会有两份存在,一个在apk里,一个在data中。这也致使多占用了一倍的空间,并且会出现各类诡异的错误。这个策略虽然和apk的瘦身无关,但它和app安装在用户手机中的大小有关,所以咱们也是须要多多留意的。
Starting from Android Studio 2.2 Preview 2 and newest build tools, the build process will automatically store native libraries uncompressed and page aligned in the APK.
在6.0+中,能够经过以下的方式进行申明:
<application
android:extractNativeLibs=”false”
...
>
复制代码
resources.arsc中存放了一个对应关系:
咱们在程序运行的时候确定要常常用到id,所以它在安装以后仍须要被频繁的读取。若是将这个文件进行压缩,在每次读取前系统都必须进行解压的操做,这就会有一些性能和内存的开销,综合考虑下这是得不偿失的。
resources.arsc的正确瘦身方式是删除没必要要的string entry,你能够借助 android-arscblamer 来检查出能够优化的部分,好比一些空的引用。
微信团队开源了一个资源混淆工具,AndResGuard。它将资源的名称进行了混淆,因此能够用它对resources.arsc进行优化,只是具体优化效果与编码方式、id数量、平均减小命名长度有关。
咱们一眼就能够知道表2确定比表1存储的字符要小,因此整个文件的大小确定也要小一些。
关于AndResGuard
这个压缩工具其实就是一个task,使用也十分简单,具体的用法请参考中文文档。
原理介绍:安装包立减1M--微信Android资源混淆打包工具
andResGuard {
mappingFile = null
use7zip = true
useSign = true
keepRoot = false
whiteList = [
//for your icon
"R.drawable.icon",
//for fabric
"R.string.com.crashlytics.*",
//for umeng update
"R.string.umeng*",
"R.string.UM*",
"R.layout.umeng*",
"R.drawable.umeng*",
//umeng share for sina
"R.drawable.sina*"
]
compressFilePattern = [
"*.png",
"*.jpg",
"*.jpeg",
"*.gif",
"resources.arsc"
]
sevenzip {
artifact = 'com.tencent.mm:SevenZip:1.1.9'
//path = "/usr/local/bin/7za"
}
}
复制代码
使用这个工具的时候须要注意一些坑,像友盟这种喜欢用反射获取资源的SDK就是一个坑(友盟的SDK就是坑王)!对于app启动图标这样的icon能够不作混淆,推荐将其放入白名单里。
META-INF文件夹中有三个文件,分别是MANIFEST.MF、CERT.SF、CERT.RSA。下面我将会列出简要的分析,若是你但愿更详尽的了解原理,能够查看《Android APK 签名文件MANIFEST.MF、CERT.SF、CERT.RSA分析》。
每个资源文件(res开头)下面都有一个SHA1-Digest的值。这个值为该文件SHA-1值进行base64编码后的结果。 若是要探究原理,能够看下SignApk.java。这个类中有一段main方法:
public static void main(String[] args) {
//...
// MANIFEST.MF
Manifest manifest = addDigestsToManifest(inputJar);
je = new JarEntry(JarFile.MANIFEST_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
manifest.write(outputJar);
//...
}
复制代码
private static void writeSignatureFile(Manifest manifest, OutputStream out)
throws IOException, GeneralSecurityException {
Manifest sf = new Manifest();
Attributes main = sf.getMainAttributes();
main.putValue("Signature-Version", "1.0");
main.putValue("Created-By", "1.0 (Android SignApk)");
BASE64Encoder base64 = new BASE64Encoder();
MessageDigest md = MessageDigest.getInstance("SHA1");
PrintStream print = new PrintStream(
new DigestOutputStream(new ByteArrayOutputStream(), md),
true, "UTF-8");
// Digest of the entire manifest
manifest.write(print);
print.flush();
main.putValue("SHA1-Digest-Manifest", base64.encode(md.digest()));
Map<String, Attributes> entries = manifest.getEntries();
for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
// Digest of the manifest stanza for this entry.
print.print("Name: " + entry.getKey() + "\r\n");
for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
print.print(att.getKey() + ": " + att.getValue() + "\r\n");
}
print.print("\r\n");
print.flush();
Attributes sfAttr = new Attributes();
sfAttr.putValue("SHA1-Digest", base64.encode(md.digest()));
sf.getEntries().put(entry.getKey(), sfAttr);
}
sf.write(out);
}
复制代码
经过代码咱们能够发现SHA1-Digest-Manifest是MANIFEST.MF文件的SHA1并base64编码的结果。
这里有一项SHA1-Digest-Manifest的值,这个值就是MANIFEST.MF文件的SHA-1并base64编码后的值。后面几项的值是对MANIFEST.MF文件中的每项再次SHA1并base64编码后的值。因此你会看到在manifest.mf中的资源名称在这里也出现了,好比abc_btn_check_material这个系统资源文件就出现了两次。
MANIFEST.MF:
CERT.SF
前者是:4XHnecusACTIgtImUjC7bQ9HNM8=,后者是YFDDnTUd6St4932sE/Xk6H0HMoc=。若是你把前一个文件打开在后面加上\n\r,而后进行编码,你就会获得CERT.SF中的值。
Map<String, Attributes> entries = manifest.getEntries();
for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
// Digest of the manifest stanza for this entry.
print.print("Name: " + entry.getKey() + "\r\n");
for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
print.print(att.getKey() + ": " + att.getValue() + "\r\n");
}
print.print("\r\n");
print.flush();
Attributes sfAttr = new Attributes();
sfAttr.putValue("SHA1-Digest", base64.encode(md.digest()));
sf.getEntries().put(entry.getKey(), sfAttr);
}
sf.write(out);
复制代码
CERT.RSA包含了公钥、所采用的加密算法等信息。它对前一步生成的MANIFEST.MF使用了SHA1-RSA算法,用开发者的私钥进行签名,在安装时使用公钥解密它。解密以后,将它与未加密的摘要信息(即,MANIFEST.MF文件)进行对比,若是相符,则代表内容没有被修改。这点和app瘦身就彻底无关了,就是android的apk签名机制。
具体的签名过程能够参考:blog.csdn.net/asmcvc/arti…
经过分析得出,除了CERT.RSA没有压缩机会外,其他的两个文件均可以经过混淆资源名称的方式进行压缩。
这里的优化会分为两块,一个是文本资源(shape、layout等)优化,还有一个就是图片资源优化。
说明:
上图中有-v4,-v21这样的文件有些是app开发者本身写的,但大多都是系统在打包的时候自动生成的,因此你只须要考虑本身项目中的drawable-mdpi、drawable-hdpi、drawable-xhdpi、drawable-xxhdpi便可。
在as的任何文件中右击,选择清除无用资源便可删除没有用到的资源文件。
不要勾选清除id!若是清除了id,会影响databinding等库的使用(id绝对占不了多少空间)
Tips: 作此操做以前,请务必产生一次commit,操做完成后必定要经过git看下diff。这样既方便查看被删除的文件,又能够利用git进行误删恢复。
shrinkResources顾名思义————收缩资源。将它设置为true后,每次打包的时候就会自动排除无用的资源(不只仅是图片)。有了它的帮忙,即便你忘记手动删除无用的资源文件也没事。
buildTypes {
release {
zipAlignEnabled true
minifyEnabled true
shrinkResources true // 是否去除无效的资源文件
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
signingConfig signingConfigs.release
}
rtm.initWith(buildTypes.release)
rtm {}
debug {
multiDexEnabled true
}
}
复制代码
大部分应用其实并不须要支持几十种语言的,微信也作了根据地区选择性下载语言包的功能。做为国内应用,咱们能够只支持中文。推荐在项目的build.gradle中进行以下配置:
android {
//...
defaultConfig {
resConfigs "zh"
}
}
复制代码
这样在打包的时候就会排除私有项目、android系统库和第三方库中非中文的资源文件了,效果仍是比较显著的。
通常raw文件下会放音频文件。若是raw文件夹下有音频文件,尽可能不要使用无损(如:wav)的音频格式,能够考虑同等质量但文件更小的音频格式。
ogg是一种较适合作音效的音频格式。移动端的音频主要是音效和短小的音频,因此淘宝大量选择了ogg格式,微博的选择格式比较多,有wav、mp三、ogg,我更加推荐淘宝的作法。固然,你仍旧不要忘记opus格式,opus也是一种有损压缩格式,若是感兴趣的话也能够尝试一下。
一个应用的界面风格是必需要统一的,这个越早作越好,最基本的就是统一颜色和按钮的按压效果。无UI设计和扁平化风格流行后,却是给应用瘦身带来了极大的的福利。界面变得越朴实,咱们能够用shape画的东西就越多。
当你的app统一过每种颜色对应的按下颜色后,接下来就须要统一按钮的形状、按钮的圆角角度、有无阴影的样子、阴影投射角度,阴影范围等等,最后还要考虑是否支持水波纹效果。
我简单将按钮分为下列元素:
上面的各个元素会产生大量的组合,shape和layer-list固然能够实现各类组合,但这样的话光按钮的背景文件就有多个,很很差维护。 通常为了开发方便,都会把须要用到的各类selector图片事先定义好,作业务的时候只须要去调用就行。但这大量的selector文件对于业务开发者来讲也是有记忆难度的,因此我推荐使用SelectorInjection这个库,它能够将上面的每一个元素进行各类组合,用最少的资源文件来实现大量的按压效果。
用库虽然好,但库也会带来学习成本,因此引入者能够将上述的组合定义为按钮的一个个的style。由于style自己是支持继承的,对于这样的组合形态来讲,继承简直是一大利器。当你的style有良好的命名后,调用者只须要知道引入什么style就行,至于你用了什么属性别人才不但愿管呢。若是业务开发中有一些特别特殊的按压状态,没有任何复用的价值,那你就能够利用库提供的丰富属性在layout文件中进行实现,不再用手忙脚乱的处处定义selector文件了。
我将不能继承和不灵活的shape变成了一个个单一的属性,经过库将多个属性进行组合,接着利用支持继承的style来将多个属性固定成一个配置文件,最后对外造成强制的规范性约束,至此便完成了减小selector文件的工做。
若是你仍是苦苦依恋着actionbar的配置模式,我推荐一个库AppBar,它可让你在用灵活的toolbar的同时也享受到配置menu的便利性。
减小layout文件有两个方法:复用和融合(include)。
把一些页面共用的布局抽出来,这不管是对layout文件的管理仍是瘦身都是极为有用的。就好比说任何一个app的list页面是至关多的,从布局层面来讲就是一个ListView或者RecyclerView,其背后还可能会有loading的view,空状态的view等等,因此个人建议是创建一个list_layout.xml,其他的list页面能够复用或者include它,这样会从很大程度上减小layout文件的数目。
对于能够被复用的layout咱们能够作统一管理,可是对于不会被复用的layout怎么办呢?假设一个页面是由两个区域组合而成的,fragment的作法是一个页面中放两个container,而后再写两个layout,但实际上这两个layout常常是没有任何复用价值的。我但愿找到一种方式,在view区块尚未复用需求的时候用一个layout搞定,须要被复用的时候也能够快速、无痛的拆分出来。
UiBlock是一个相似于fragment的解耦库,它能够为同一个layout中不一样区域的view进行逻辑解耦(由于layout可预览的特性,ui定位方面不是难题),它能帮咱们尽量少的创建layout文件。 若是将来需求发生了变更,layout文件中的一块view须要抽出成独立的layout文件的时候,UiBlock的逻辑代码几乎不用改动,你只须要把抽出的layout文件include进来,而后在include标签上定义一个id便可。而这个工做能够经过as的重构功能自动完成,毫不拖泥带水。
<!-- 使用include -->
<include
android:id="@+id/bottom_ub"
layout="@layout/demo_uiblock"
android:layout_width="match_parent"
android:layout_height="100dp"
/>
复制代码
public void addHeaderToListView(ListView listView, View header) {
if (header == null) {
throw new IllegalArgumentException("Can't add a null header view to ListView");
}
ViewGroup viewParent = (ViewGroup) header.getParent();
viewParent.removeView(header);
AbsListView.LayoutParams params = new AbsListView.LayoutParams(
header.getLayoutParams().width,
header.getLayoutParams().height);
header.setLayoutParams(params);
listView.addHeaderView(header); // add
}
复制代码
我将listView和它的没有复用价值的header放到了同一个layout中,而后在activity中利用上述代码进行了操做,最终完成了用一个layout文件给listView加头的工做。
作过滤镜和贴纸的同窗应该会注意到贴纸、表情这类的东西是至关大的,对于这类的图片资源我强烈建议经过在线商店进行获取。这样既可让你踏踏实实的卖贴纸,又能够减少应用的大小。这么作虽然有必定的复杂度和出错几率,但投入产出比仍是很不错的。
这个虽然不算是app大小的优化,可是若是你放错了图片,对于app启动时的内存大小会有必定的影响:
思考一下,若是把一个原本应该放在drawable-xxhdpi里面的图片放在了drawable文件夹中会出现什么问题呢? 在xxhdpi设备上,图片会被放大3倍,图片内存占用就会变为原来的9倍!
国内也有不少人说能够用一套图片来作,不用出多套图,借此来达到app瘦身和给设计减负的目的。谷歌官方是建议为不一样分辨率出不一样的图片,为此国内也有很多文章讨论过这件事情,这篇总结的不错推荐一读。
每次说到这个话题的时候总有不少人有不一样的见解,何况不少人还不知道.9图也是须要切多份的,因此这里我仍是先分析一下大厂的放图策略,最后我们再讨论下较优的方案。
mdpi中存留了一些android原始的icon,这个从命名和前缀就能看出来。经过图片大小分析,这个目录下面都是一些很小的icon,还有一些没有用到的icon(这个launcher图片也很好的说明了淘宝的历史)。
hdpi:
hdpi中分为两部分:表情和其余图片。f+数字的图片都是表情图片,淘宝仅仅有一套表情图片,而且都放在这个目录下。除了少许的图片和mdip的图片一致(好比用户头像的place_holder)外,其他的图片和mdpi的图片彻底不一样。顺便说一下,此目录下除了表情以外,其他的都是一些小icon,绝对属于尺寸很小的那类。
xhdpi:
xhdpi又和hdpi不一样了,它里面有大量的国家icon。除此以外就是一些对清晰度要求较高的icon。
xxhdpi:
xxhdpi就没什么东西了,几张图而已。
其余:
有后缀的文件夹中除了5张左右的淘宝本身的icon外,其他都是系统的图片,均以abc开头。我不清楚淘宝到底有没有使用到这些图片,但我能够确定地说其中有着冗余图片,或许有着进一步优化的方案。
总结: 淘宝的放置图片策略是大量的图片在hdpi,xhdpi中,好比表情图在hdpi中,国家图在xhdpi中,大多数图片都仅有一套,少数全局的icon是会有多张的状况(极少,估计只有十几张)。xxhdpi和mhdpi仅仅做为补充,没有太大的做用。
淘宝最使人好奇的点在于它的资源文件很小,可是so文件至关大:
微博是一个典型的android风格的app,它的drawable全都是有后缀的,彻底符合安卓标准的默认打包策略,它还有根据像素密度的图片,甚至有ldpi的目录。
mdpi-v4
mdpi中有大量的小icon,里面有个叫作share_wx的,从名字一会儿就知道是微信分享的icon,但实际是微博的logo,比较有趣。其他的都是一些边边角角的图标,量不大,因此主力图确定不在这里。
hdpi-v4
这是微博图片存放的主要目录,有不少大背景和表情,微博的表情图片和淘宝同样都是在hdpi中的,它以lxh,emoji等前缀开头,用来区分不一样风格的表情。
xhdpi-v4和xxhdpi-v4*
这里放了一些背景大图,我也发现了大量和hdpi中同样的图片,因此能够大胆的假设微博是作了不一样像素的图片的。这也证明了个人想法——微博是很标准的android应用。
ldpi-v4
这个目录的确没什么用,微博自身也不会维护这个目录,这全都是第三方库和应用商店给的图片,微博开发者只须要放进来就好。
sw400dp和sw32dp-400dp
这些目录放了一些为不一样分辨率准备的长得相同的icon,固然还有微博本身的logo。
经过上面的分析,咱们是否是能够得出一些经验了呢?
ok,如今我们就能够来看微信的资源了,混淆怕啥,友盟混淆的abcd代码都能看懂,微信的adcd资源也应该不难。
raw(a9)
这个目录中放置了大量的svg图片和mp3文件,从专业的角度来想,drawable目录下确定不会放mp3,因此这个确定是raw文件了。这个目录下有大量的svg,因此能够看出微信已经实现了全svg化,而且已经在线上稳定运行了。这点在微信早期的公众号上也能够获得佐证,详细请看Android微信上的SVG。
文中提到了:
第一步,拿到.svg后缀的资源文件(UI很容导出这种图片),放在raw目录下而不是drawable目录。 第二步,把 R.drawable.xxx 换成 R.raw.xxx;把 @drawable/xxx 换成 @raw/xxx。
layout(f)
如今有了两个线索,那么初步估计上面的三个目录确定是咱们常见的目录,不然不会那么大。
打开f后发现这个就是layout文件,因此其他的确定就是图片文件了。
hdpi(y)
y是2.9m,里面有大量的表情,因此我判断它是hdpi,为了更加证明这个猜想,我找到了两张相同的图片。
a2中:
y中:
y中图片是11kb,a2中图片是76kb,这明显说明y是hdpi,a2是xhdpi。
xhdpi(a2):
xhdpi中的图片和hdpi中的图片相同的很少,微信在这里放的是一些大图。
drawable(k)
drawable自己没啥能够说的,可是微信的drawable中.9图份额不多,因此我在想是否svg能够在必定的程度上完成一些.9的工做呢?
总结:
经过分析得出,传统的出多个分辨率图片的作法在大厂中已经发生了改变,阿里系、腾讯系的产品都采用了一套图走天下的路子。这样的作法仍是有利有弊的,权衡之下我给出以下建议:
想要作好图片的优化工做最重要的一点是知道应该选择什么样的图片格式,对于这点我推荐一个视频,方便你们进行深刻的了解。
这是谷歌给出的建议,简单来讲就是:VD->WebP->Png->JPG
VD即VectorDrawable,android上的svg实现类。在经历了长达半年的缓慢兼容之路后,如今终于被support库兼容了,官方文档中给出了这样一个例子:
// Gradle Plugin 2.0+
android {
defaultConfig {
vectorDrawables.useSupportLibrary = true
}
}
复制代码
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_add"
/>
复制代码
配置好后,咱们就能够利用强大的svg来替换纯色icon了。
先去这里下载svg图片:icomoon.io/app/#/selec…
而后利用这个在线工具转换成VectorDrawable。
support库的代码质量仍是不错的,可是svg毕竟是一个图片格式,因此使用svg前仍是须要格外慎重的。我写了一个demo,把用到的全部属性都作了示例,而后利用云测服务进行兼容性测试。
测试svg也挺简单的,首先看会不会崩溃,而后看各个分辨率、各个api下是否会有显示不正常的状况,若是都ok,那么就能够准备引入到项目里面了。 具体的测试代码在SelectorInjection,我测试下来100%经过。
svg图片是有默认宽高的,设计也会给出一个默认宽高,设置一个合适的默认宽高对之后的图片复用会有很大帮助。
TextView中drawableLeft等属性是不能设置图片的宽高的,但ImageView能够。若是你的图片会被复用,建议将图片的宽高设置为TextView中的drawable宽高。
ImageView中的svg默认状况下是会随着控件的大小而改变的,它不会像png那样保持本身的原始大小。咱们能够利用这一特性,再配合padding和scaleType属性来完成各类效果。
<ImageView
android:layout_width="60dp"
android:layout_height="60dp"
android:tint="@color/blue"
app:srcCompat="@drawable/facebook"
/>
<ImageView
android:layout_width="30dp"
android:layout_height="30dp"
android:tint="@color/orange"
app:srcCompat="@drawable/facebook"
/>
<ImageView
android:layout_width="60dp"
android:layout_height="60dp"
android:scaleType="centerInside"
android:tint="@color/red"
app:srcCompat="@drawable/facebook"
/>
<ImageView
android:layout_width="60dp"
android:layout_height="60dp"
android:padding="10dp"
android:tint="@color/green"
app:srcCompat="@drawable/facebook"
/>
复制代码
svg的支持是要经过app:srcCompat这个属性来作的,若是稍微一不注意写成了src,那么就会出现低版本手机上不兼容的问题。你能够尝试经过配置Lint规则或是利用脚本进行文件的遍历等方式来防止出现因开发写错属性而崩溃的问题。
将svg放入selector中的时候可能会出现一些问题,stackoverflow上也给出了解决方案,就是下面这段代码放在Activity中。
static {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
复制代码
开启这个flag后,你就能够正常的使用Selector这样的DrawableContainers了。
AppCompatActivity会自动将xml文件中的ImageView替换为AppCompatImageView,可是若是用了你的自定义控件,那么这种机制就无效了,因此自定义控件中尽可能使用AppCompatImageView来代替ImageView,使用 setImageResource()来设置资源。若是你的自定义控件中须要得到drawable或者是有自定义需求,那么能够参考AppCompatImageView中的svg的helper类来编写。
public class AppCompatImageHelper {
private final ImageView mView;
public AppCompatImageHelper(ImageView view) {
mView = view;
}
public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
TintTypedArray a = null;
try {
Drawable drawable = mView.getDrawable();
if (drawable == null) {
a = TintTypedArray.obtainStyledAttributes(mView.getContext(), attrs,
R.styleable.AppCompatImageView, defStyleAttr, 0);
// If the view doesn't already have a drawable (from android:src), try loading // it from srcCompat final int id = a.getResourceId(R.styleable.AppCompatImageView_srcCompat, -1); if (id != -1) { drawable = AppCompatResources.getDrawable(mView.getContext(), id); if (drawable != null) { mView.setImageDrawable(drawable); } } } if (drawable != null) { DrawableUtils.fixDrawable(drawable); } } finally { if (a != null) { a.recycle(); } } } public void setImageResource(int resId) { if (resId != 0) { final Drawable d = AppCompatResources.getDrawable(mView.getContext(), resId); if (d != null) { DrawableUtils.fixDrawable(d); } mView.setImageDrawable(d); } else { mView.setImageDrawable(null); } } boolean hasOverlappingRendering() { final Drawable background = mView.getBackground(); if (Build.VERSION.SDK_INT >= 21 && background instanceof android.graphics.drawable.RippleDrawable) { // RippleDrawable has an issue on L+ when used with an alpha animation. // This workaround should be disabled when the platform bug is fixed. See b/27715789 return false; } return true; } } 复制代码
市面上有不少优秀的图片加载库,它们通常都会支持多种图片路径的加载,好比磁盘图片,网络图片,res图片等等,对于svg这样的图片格式,它们是否支持就要你们结合本身的图片框架进行调研了。
关于动画和性能方面的问题,《Android Vector曲折的兼容之路》中给出了具体的示例和建议:
Bitmap的绘制效率并不必定会比Vector高,它们有必定的平衡点,当Vector比较简单时,其效率是必定比Bitmap高的,因此为了保证Vector的高效率,Vector须要更加简单,PathData更加标准、精简,当Vector图像变得很是复杂时,就须要使用Bitmap来代替了。 Vector适用于icon、Button、ImageView的图标等小的icon,或者是须要的动画效果,因为Bitmap在GPU中有缓存功能,而Vector并无,因此Vector图像不能作频繁的重绘。 Vector图像过于复杂时,不只仅要注意绘制效率,初始化效率也是须要考虑的重要因素。这点能够参考微信中的svg。 SVG加载速度会快于PNG,但渲染速度会慢于PNG,毕竟PNG有硬件加速,但平均下来,加载速度的提高弥补了绘制的速度缺陷。
svg是没办法在文件目录下进行预览的,其放置的目录也和其余图片不一样,若是没有作好管理工做,将来的drawable目录就会变得愈加的混乱。其实,对于目录或者包内文件的管理有个很简单的原则:同目录多类型文件,之前缀区分;不一样目录,同类型文件,以意义区分。
drawable目录下有多种类型的文件,咱们利用英文排序的原则将这些文件简单分为svg、.9图、shape、layer-list这几类。
经过规范特定的前缀,就能够造成一个便于查找和理解的目录树,以达到分类的目的。
在特定前缀的规范下,次级分类的命名就能够按照功能或用途来作区分,好比button或share的icon就能够用不一样的前缀来标识。强烈建议开发和设计定一个命名标准,这样开发就不用对设计出的图片进行重命名了,并且还能够保证两个部门有一致的认知。
这个仅仅是一个简单的分类方法,在实际中须要灵活使用。若是一些文件都是用于某种特定类型的,那么能够自定义前缀。好比我对于按钮使用的形状就用了btn做为前缀,而忽略了它们自己的文件类型。
svg是一个特殊格式的文件,可预览性大大低于png等经常使用的图片格式,但幸亏win下能够直接在文件目录下预览svg图像,效果十分不错。
webp做为一种新的图片格式,从Android4.0+开始原生支持,可是不支持包含透明度,直到4.2.1+才支持显示含透明度的webp,使用的时候要特别注意。 webp相比于png最明显的问题是加载稍慢,不过如今的智能设备硬件配置愈来愈高,这点差别愈来愈小。腾讯以前有一篇对于webp的分析文十分不错,若是你准备要用webp了,那么它绝对值得一看。
**注意:**若是你的项目最低支持到4.2.1,那么你能够继续阅读了,若是项目还须要支持到4.0版本,我建议暂时不要上webp,成本过高。
咱们能够经过智图或者isparta将其它格式的图片转换成webP格式。
兼容性很差
官方文档中说只有在4.2.1+以上的机型,才能解析无损或者有透明度调整的webp图片,4.0+才开始支持无透明度的webp图片。我经过云测发现,在4.0~4.2.1的系统中,带有透明度的webp图片虽然不会崩溃,可是彻底没法显示。
《APK瘦身记,如何实现高达53%的压缩效果》一文中也提到有alpha值的jpg图片,通过webp转换后,没法在4.0,4.1的Android系统上运行的问题,具体缘由见官方文档:
除了兼容性问题外,webp在某些机型和rom上可能会出现一些“神奇”的问题。在三星的部分机型上,部分有alpha通道的图中会有一条很明显的黑线(三星的rom对于shape的alpha的支持也有问题,是红线)。在小米2刷成4.xx的手机上,系统未能正确识别xml文件中描述的webp图片,也会致使加载webp失败。
不便于预览
由于webp的图片格式是很难预览的,as也没有办法直接预览webp格式,我通常是经过chrome浏览器打开webp,十分不方便。
咱们知道gradle在build时,有一个mergeXXXResource Task,它将项目的各个aar中全部的res资源统一整合到/build/intermediates/res/flavorName/{buildType}目录下。
webpConvertPlugin这个gradle插件能够在mergeXXXResource Task和processXXXResource Task之间插入一个task,这个task会将上述目录下的drawable进行统一处理,将项目目录里的png、jpg图片(不包含.9图片,webp转换后显示效果不佳)批量处理成webp图片,这样可让咱们在平常开发时用png、jpg,正式发包时用webp。
复用相同的icon
咱们经过svg可让一张图片适用于不一样大小的容器中,以达到复用的目的。最多见的例子就是“叉”,除非你的x是有多种颜色的,那么这种表示关闭的icon能够复用到不少地方。
上图中我经过组合的方式将长得同样的icon(facebook、renren等)复用到了不一样的界面中,不只实现了效果,可维护性也不错。
使用Tint
着色器(tint)是一个强大的工具,我将其和shape、svg等结合后产生了化学反应。TintMode共有6种,分别是:add,multiply,screen,srcatop,srcin(默认),src_over。下图是一篇文章中的总结,说明了其灵活性
通常用默认的模式就能够搞定大多数需求了,使用到的控件主要是TextView和ImageButton。ImageButton官方已经给出了支持方案,TextView由于有四个Drawable,官方的tint属性在低版本又不可用,因此我让SelectorTextView支持了一下。若是你想要了解具体的兼容方法,能够参考库代码或《Drawable 着色的后向兼容方案》。
ImageButton
android:tint="@color/blue"
复制代码
SelectorTextView
app:drawableLeftTint="@color/orange"
app:drawableRightTint="@color/green"
app:drawableTopTint="@color/green"
app:drawableBottomTint="@color/green"
复制代码
由于我用了SelectorTextView和SelectorImageButton,因此我对于背景的tint没有什么需求,也就没作兼容性测试,有兴趣的同窗能够尝试一下。若是你决定要采用tint,必定要经过云测等手段作下兼容性测试,下图是我对于上述属性的测试结果:
一个应用中的list页面都应该作必定程度的统一,对于有限长度的list,咱们可能偏向于用ScrollView作,对于无限长的list用RecyclerView作,但对于它们的按压效果我强烈建议采用同一个样式。
以微信为例,它的全部列表都是白色的item,个人优化思路以下:
若是一个icon能够经过另外一个icon的旋转变换来获得,那么咱们就能够经过以下方法来实现:
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/blue_btn_icon" // 原始icon
android:fromDegrees="180" // 旋转角度
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="180" />
复制代码
图片的压缩策略是:
优先压大图,后小图 不压.9图(svg在侠义上不算图) 对于开屏大图片的压缩需注意力度,要和设计确认后再作 对于体积特别大(超过50k)的图片资源能够考虑有损压缩 关于如何量化两张图片在视觉上的差异,Google 提供了一个叫butteraugli的工具,有兴趣的同窗能够尝试一下。
ImageOptim
mac上超好用的图片压缩工具是ImageOptim,它集成了不少好用图片压缩库,不少blog中的图片也是用它来压缩的。
值得一提的是,借助Zopfli,它能够在不改变png图像质量的状况下使图片大小明显变小。
pngquant
pngquant也是一款著名的压缩工具,对于png的疗效还不错,但它不必定就适合app中那种背景透明的小icon,因此对比起tinypng来讲,优点不明显。
tinypng
tinypng是一款至关著名的商用压缩工具,tinypng提供了开放接口供开发者开发属于本身的压缩工具(付费服务)。tinypng对于免费用户也算友好,每个月能够免费压缩几百张图片。
我用gradle插件来使用tinypng,更加简单方便。我通常的作法是发版本前才作一次图片压缩,每次debug的时候是直接跳过这个task的,彻底不影响平常的debug。
tinyinfo {
apiKey = 'xxxxxxxxx'
//编译时是否跳过此task
skip = true
//是否打印日志
isShowLog = true
}
复制代码
有人说tinypng的缺点是在压缩某些带有过渡效果(带alpha值)的图片时,图片可能会失真,对于这种图片你能够将png图片转换为webP格式。
aapt默认会在打包时进行图片的压缩工做(不管你知不知道,它一直在默默的工做),若是你已经作了图片压缩了,那么建议手动禁止这个功能,不然“可能会”出现图片二次压缩后反而变大的状况,缘由请看:Smaller PNGs, and Android’s AAPT tool。
android {
defaultConfig {
//...
}
aaptOptions {
cruncherEnabled = false
}
}
复制代码
dex自己的体积仍是很可观的,虽然说代码这东西不占用多少存储空间,可是微信这样的大厂的dex已经达到了20多M。我大概估计了一下,若是你没有达到方法数上限,那么你的dex的大小大约是10M。纵观应用市场,没有用multiDex的又有几家呢?
dexcout
要优化这部分,首先须要对公司的、android库的、第三方库的代码进行深刻的了解,我用了dexcount来记录项目的方法数:
dexcount {
format = "list"
includeClasses = false
includeFieldCount = true
includeTotalMethodCount = false
orderByMethodCount = false
verbose = false
maxTreeDepth = Integer.MAX_VALUE
teamCityIntegration = false
}
复制代码
经过分析你能够知道代码的具体状况了,好比某个第三方库是否已经不用了、本身项目的哪一个包的方法数最多、目前代码状况是否合理等等。
statistic
我是经过Statistic这个as插件来评估项目中开发人员写的代码量的,它生成的报表也不错:
如今我能够知道: 哪些类空行数太多,是否是没有按照代码规范来; 哪些类的代码量不多,是否有存在的必要; 哪些类行数过多,是否没有遵照单一职责原则,是否能够进行进一步的拆分
apk method
你还能够用apk-method-count这个工具来查看项目中各个包中的方法数,它会生成树形结构的文档,十分直观。
若是你想删掉没有用到的代码,能够借助as中的Inspect Code对工程作静态代码检查。
Lint是一个至关强大的工具,它能作的事情天然不限于检查无用资源和代码,它还能检测丢失的属性、写错的单位(dp/sp)、放错像素目录的图片、会引发内存溢出的代码等等。从eclipse时代发展到如今,lint真的是愈来愈方便了,咱们如今只须要点一点就行。
注意:
这种删除无用代码的工做须要反复屡次进行(好比一月一次)。当你删除了无用代码后,这些代码中用到的资源也会被标记为无用,这时就能够经过上文提到的Remove Unused Resources来删除。
手动删除无用代码的流程太繁琐了,若是是一两次倒还会带来删除代码的爽快感,但若是是专人机械性持续工做,那我的确定要疯的。为了保证每次打包后的apk都包含尽量少的无用代码,咱们能够在build.gradle中进行以下配置:
android {
buildTypes {
release {
minifyEnabled true // 是否混淆
}
}
}
复制代码
虽然这种方式成果显著,但也须要配合正确的proguard配置才能起做用,推荐看下读懂 Android 中的代码混淆一文。
这种利用混淆来删除代码的方式是一种保险措施,真正治本的方法仍是在开发过程当中随手删除无用的代码,毕竟开发者才是最清楚一段代码该不应被删的。
咱们在测试的时候可能会随便写点测试方法,好比main方法之类的,而且还会引入一些测试库。对于测试环境的代码gradle提供了很方便的androidTest和test目录来隔离生产环境。 对于测试时用到的大量库,能够进行test依赖,这样就能够保证测试代码不会污染线上代码,也能够防止把测试工具、代码等发布到线上等错误(微博就干过这样的事情)。
// Dependencies for local unit tests
testCompile 'junit:junit:4.12'
testCompile 'org.hamcrest:hamcrest-junit:2.0.0.0'
// Android Testing Support Library's runner and rules androidTestCompile 'com.android.support:support-annotations:24.1.1' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:rules:0.5' // Espresso UI Testing androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) 复制代码
PS:在layout中利用tools也是为了达到上述目的。
debug模式是开发者的调试模式,这个模式下log全开,而且会有一些帮助调试的工具(好比:leakcanary,stetho),咱们能够经过debugCompile和releaseCompile来作不一样的依赖,有时候也会须要no-op(关于no-op的内容能够参考下开发第三方库最佳实践)。
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
}
复制代码
debug和release是android自己自带的两种生产环境,在实际中咱们可能须要有多个环境,好比提测环境、预发环境等,我以rtm(Release to Manufacturing 或者 Release to Marketing的简称)环境作例子。
首先在目录下建立rtm文件:
复刻release的配置:
buildTypes {
release {
zipAlignEnabled true
minifyEnabled true
shrinkResources true // 是否去除无效的资源文件
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
signingConfig signingConfigs.release
}
rtm.initWith(buildTypes.release)
rtm {}
debug {
multiDexEnabled true
}
}
复制代码
配置rtm依赖:
ext {
leakcanaryVersion = '1.3.1'
}
dependencies {
debugCompile "com.squareup.leakcanary:leakcanary-android:$leakcanaryVersion"
rtmCompile "com.squareup.leakcanary:leakcanary-android-no-op:$leakcanaryVersion"
releaseCompile "com.squareup.leakcanary:leakcanary-android-no-op:$leakcanaryVersion"
}
复制代码
rtm环境天然也有动态替换application文件的能力,我为了方便非开发者区分app类别,我作了启动icon的替换。
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.kale.example"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
>
<application
android:name=".RtmApplication"
android:allowBackup="true"
android:icon="@drawable/rtm_icon"
tools:replace="android:name,android:icon"
/>
</manifest>
复制代码
如今我能够将环境真正须要的代码打包,不须要的代码所有剔除,以达到瘦身的目的。
谷歌最近有意将support-v4库进行拆分,可是无奈v4被引用的地方太多了,但这不失为一个好的开始。目前来看使用拆分后的support库是没有什么优势的,因此我也不建议如今就开始动手,当谷歌和第三方库做者都开始真的往这方面想的时候,你再开始吧。
mulitdex会进行分包,分包的结果天然比原始的包要大一些些,能不用mulitdex则不用。但若是方法数超了,除了插件化和RN动态发包等奇淫巧技外我也没什么好办法了。
同一功能就用一个库,禁止一个app中有多个网络库、多个图片库的状况出现。若是一个库很大,而且申请了各类权限,那么就去考虑换掉他。
话人人都会说,但若是一个项目是由多个项目成员合做完成的,很难避免重复引用库的问题,同一个功能用不一样的库,或者一个库用不一样版本的现象比比皆是,这也是很难去解决的。个人解决方案是部门之间多沟通,尽可能作base层,base层由少数人进行维护,正如微信在so库层面的作法:
C++运行时库统一使用stlport_shared 以前微信中的C++运行库大多使用静态编译方式,使用stlport_shared方式可减少APK包大小,至关于把你们公有的代码提取出来放一份,减小冗余。同时也会节省一点内存,加载so的时候动态库只会加载一次,静态库则随着so的加载被加载多分内存映像。 把公用的C++模块抽成功能库 其实与上面的思路是一致的,主要为了减小冗余模块。你们都用到的一些基础功能,应该抽成基础模块。