android减包小工具----gradle任务分析

导读 –android减包小工具projectFit.gradle,基于android Gradle的打包配置任务以及APK包进行减包分析,并生成合理的减包建议报告文件HTML

1、分析指标

1、混淆配置
2、资源优化配置
3、打包优化配置
4、微信资源混淆
5、检查支持的abi系列
6、查找没有任何引用的资源文件
7、检查aapt没有进行压缩优化的资源文件

2、使用方法

(1)在项目主模块(一般是app modlue)引进projectfit.gradle文件
(2)在主模块build.gradle配置projectfit.gradle

1
apply from: 'projectfit.gradle'

(3)修改projectfit.gradle的减包分析输出文件以及分析的APK包,默认项目根目录输出projectfit.html文件

1
2
3
4
5
6
ext {
//输出的报告文件目录
reportPath = project.rootDir.path + File.separator + "projectfit.html"
//build/output/apk下需要分析的应用包名,如需要配置绝对路径的APK包,可以直接修改函数getApkFile()
apkName = "app-pushDev-debug.apk"
}

(4)在Android Studio的Terminal 控制面板输入:gradlew projectfit 即可

1
app>gradlew projectfit

建议 在分析减包前先编译一下项目可以达到更好的效果
维护:linzhenhua@ppmoney.com

3、附projectfit.gradle源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
import java.util.regex.Matcher
import java.util.regex.Pattern

//apk 减包检查工具化 Lin


ext {
//输出报告文件目录
reportPath = project.rootDir.path + File.separator + "projectfit.html"
//build/output/apk下需要分析的应用包名,如需要配置绝对路径的APK包,可以直接修改函数getApkFile()
apkName = "app-pushDev-debug.apk"


//输出报告的基本格式
TAG_TITLE_BEGIN = '<p>'
TAG_TITLE_END = '</p>'
TAG_CONTENT_BEGIN = '<H6><p>'
TAG_CONTENT_END = '</H6></p>'
TAG_LIST_BEGIN = '<p>'
TAG_LIST_END = '</p>'
TAG_SPECIAL_BEGIN = '<span style="color: red;">'
TAG_SPECIAL_END = '</span>'
}



task projectFit << {
File reportFile = file(new File(reportPath))
if (!reportFile.exists()) {
reportFile.createNewFile()
}
FileWriter out = new FileWriter(reportFile)
writeInfo(out, '<!DOCTYPE html>')
writeInfo(out, '<html>')
writeInfo(out, '<head>')
writeInfo(out, '<title>PP APK减包分析及建议工具 </title>')
writeInfo(out, '<body>')
writeBaseInfo(out)
checkMinifyEnabled(out)
checkShrinkResources(out)
checkAndResGuardConfig(out)
checkZipAlignEnabled(out)
checkSoLibsSupport(out)
findunuseResourceFiles(out)
findUnDeflateFiles(out)
writeInfo(out, '</body>')
writeInfo(out, '</html>')
out.flush()
out.close()
}

def writeBaseInfo(def out) {
writeInfo(out, "<h5><p>APK : $TAG_SPECIAL_BEGIN $apkName $TAG_SPECIAL_END </p></h5>")
}

//获取分析的apk文件,默认取out/apk下面的-release.apk包
def getApkFile() {
def apkPath = project.rootDir.path + File.separator + 'app' + File.separator + 'build' + File.separator + 'outputs' + File.separator + 'apk';
def apkPathFinal = apkPath + File.separator + apkName
File apkFile = new File(apkPathFinal)
return apkFile
}

//是否开启资源优化
def checkShrinkResources(def out) {
def begin = "$TAG_TITLE_BEGIN Begin check shrinkResources $TAG_TITLE_END"
writeInfo(out, begin)
def open = project.android.buildTypes.release.isShrinkResources();
def result = "$TAG_CONTENT_BEGIN 是否开启资源优化: $open $TAG_CONTENT_END"
writeInfo(out, result)
if (!open) {
def range = getReduceMinAndMax(0.01, 0.05)
writeInfo(out, "$TAG_CONTENT_BEGIN if set shrinkResources true,the package size will be reduced about $TAG_SPECIAL_BEGIN ${range['min']}~${range['max']} $TAG_SPECIAL_END $TAG_CONTENT_END")
}
def end = "$TAG_TITLE_BEGIN End check shrinkResources $TAG_TITLE_END"
writeInfo(out, end)
}

//是否开启包优化
def checkZipAlignEnabled(def out) {
def begin = "$TAG_TITLE_BEGIN Begin check ZipAlign $TAG_TITLE_END"
writeInfo(out, begin)
def open = project.android.buildTypes.release.isZipAlignEnabled();
def result = "$TAG_CONTENT_BEGIN 是否开启包优化: $open $TAG_CONTENT_END"
writeInfo(out, result)
if (!open) {
def range = getReduceMinAndMax(0.01, 0.03)
writeInfo(out, "$TAG_CONTENT_BEGIN if set shrinkResources true,the package size will be reduced about $TAG_SPECIAL_BEGIN ${range['min']}~${range['max']} $TAG_SPECIAL_END $TAG_CONTENT_END")
}
def end = "$TAG_TITLE_BEGIN End check ZipAlign $TAG_TITLE_END"
writeInfo(out, end)
}

//是否开启混淆
def checkMinifyEnabled(def out) {
def begin = "$TAG_TITLE_BEGIN Begin check minifyEnabled $TAG_TITLE_END"
writeInfo(out, begin)
def open = project.android.buildTypes.release.isMinifyEnabled();
def result = "$TAG_CONTENT_BEGIN 是否开启混淆: $open $TAG_CONTENT_END"
writeInfo(out, result)
if (open) {
def range = getReduceMinAndMax(0.05, 0.10)
writeInfo(out, "$TAG_CONTENT_BEGIN if set shrinkResources true,the package size will be reduced about $TAG_SPECIAL_BEGIN ${range['min']}~${range['max']} $TAG_SPECIAL_END $TAG_CONTENT_END")
}
def end = "$TAG_TITLE_BEGIN End check minifyEnabled $TAG_TITLE_END"
writeInfo(out, end)
}

//支持的SO库类型
def checkSoLibsSupport(def out) {
def begin = "$TAG_TITLE_BEGIN Begin check include So Libs $TAG_TITLE_END"
writeInfo(out, begin)
def suportAbiFilters = "$TAG_CONTENT_BEGIN suport abi filter: $TAG_CONTENT_END"
writeInfo(out, suportAbiFilters)
def onlySupportArmeabi = 0 //除了Armeabi外其他包的总大小
def hasOtherFilter = false //是否支持除了Armeabi外其他的filter
def soLibs = ['armeabi', 'armeabi-v7a', 'x86', 'x86_64', 'mips', 'arm64-v8a', 'mips64']
soLibs.each { key ->
def libs = findApkAbiFilterFile(key)
def abiSize = 0;
if (!libs.empty) {
writeInfo(out, "$TAG_CONTENT_BEGIN $key -> $TAG_CONTENT_END")
libs.each { value ->
writeInfo(out, TAG_LIST_BEGIN + value['name'] + ' size:' + getUnitValue(value['size']) + TAG_LIST_END)
abiSize += value['size']
}
writeInfo(out, TAG_CONTENT_BEGIN + TAG_SPECIAL_BEGIN + key + TAG_SPECIAL_END + ' lib total size:' + TAG_SPECIAL_BEGIN + getUnitValue(abiSize) + TAG_SPECIAL_END + TAG_CONTENT_END)
if (!key.equalsIgnoreCase('armeabi')) {
hasOtherFilter = true
onlySupportArmeabi += abiSize
}
}
}
if (hasOtherFilter) {
writeInfo(out, TAG_CONTENT_BEGIN + 'if only support armeabi,you could reduce package size ' + TAG_SPECIAL_BEGIN + getUnitValue(onlySupportArmeabi) + TAG_SPECIAL_END + TAG_CONTENT_END)
}
def end = "$TAG_TITLE_BEGIN End check include So Libs $TAG_TITLE_END"
writeInfo(out, end)
}

//查找apk里面的Lib文件,filter:armeabi,x86_64,armeabi-v7a
def findApkAbiFilterFile(def filter) {
def apkFile = getApkFile();
def aaptCmd = aaptExe() + ' l -v ' + apkFile?.path
def filterStr = 'lib/' + filter
Set<Map> filterFiles = new HashSet<>();
BufferedReader bf = new BufferedReader(new InputStreamReader(aaptCmd.execute().inputStream))
String line
while ((line = bf.readLine()) != null) {

if (line.startsWith(filterStr, 68)) {
def map = getAAPTLineInfo(line);
filterFiles.add(map)
}
}
bf.close()
return filterFiles
}

//检查配置微信资源混淆
def checkAndResGuardConfig(def out) {
if (out != null) {
def begin = "$TAG_TITLE_BEGIN Begin check AndResGuard $TAG_TITLE_END"
writeInfo(out, begin)
def hasConfig = project.pluginManager.hasPlugin('AndResGuard')
def result;
if (hasConfig) {
result = "$TAG_CONTENT_BEGIN 是否配置微信资源混淆: true $TAG_CONTENT_END"
} else {
def range = getReduceMinAndMax(0.10, 0.15)
result = "$TAG_CONTENT_BEGIN Config AndResGuard could reduce package size $TAG_SPECIAL_BEGIN ${range['min']}~${range['max']} $TAG_SPECIAL_END,please consult https://github.com/shwenzhang/AndResGuard $TAG_CONTENT_END"
}
writeInfo(out, result)
def End = "$TAG_TITLE_BEGIN End check AndResGuard $TAG_TITLE_END"
writeInfo(out, End)
}
}

//筛选没有用的资源文件
def findunuseResourceFiles(def out) {
def resRootPath = project.rootDir.path + File.separator + 'app' + File.separator + 'build' + File.separator + 'outputs' + File.separator + 'mapping'
fileListFind(resRootPath, out)
}

def fileListFind(String path, def out) {
File root = new File(path)
File[] files = root.listFiles()
files.each { item ->
if (item.isDirectory()) {
fileListFind(item.absolutePath, out)
} else {
if ('resources.txt'.equalsIgnoreCase(item.name)) {
findUnuseResourceList(item.absolutePath, out)
}
}
}
}

def findUnuseResourceList(def filePath, def out) {
File resourceFile = new File(filePath)
String line;
int count = 0;
def totleNewSize = 0;
def totleOldSize = 0;
def begin = "$TAG_TITLE_BEGIN Begin Find UnUse Resource Files $TAG_TITLE_END"
writeInfo(out, begin)
if (resourceFile.exists()) {
BufferedReader bf = new BufferedReader(new InputStreamReader(new FileInputStream(resourceFile)))
while ((line = bf.readLine()) != null) {
if (line.contains('Skipped unused resource')) {
def map = getResourceUnuseFileInfo(line)
writeInfo(out, TAG_LIST_BEGIN + map['name'] + TAG_LIST_END)
count++
totleNewSize += map['newSize']
totleOldSize += map['oldSize']
}
}
def result = "$TAG_CONTENT_BEGIN find $TAG_SPECIAL_BEGIN $count $TAG_SPECIAL_END resource files unUse,total size: $TAG_SPECIAL_BEGIN" + getUnitValue(totleOldSize) + TAG_SPECIAL_END + TAG_CONTENT_END
writeInfo(out, result)
def advices = "$TAG_CONTENT_BEGIN remove the unuses files could reduce the package size: $TAG_SPECIAL_BEGIN" + getUnitValue(totleNewSize) + TAG_SPECIAL_END + TAG_CONTENT_END
writeInfo(out, advices)
bf.close()

} else {

def advice = "$TAG_CONTENT_BEGIN please check the file /outputs/mapping/\$productFlavors/resources.txt $TAG_CONTENT_END"
writeInfo(out, advice)
}
def End = "$TAG_TITLE_BEGIN End Find UnUse Resource Files $TAG_TITLE_END "
writeInfo(out, End)
}

//筛选aapt没有压缩优化的文件
def findUnDeflateFiles(def out) {
def apkFile = getApkFile();
def aaptCmd = aaptExe() + ' l -v ' + apkFile?.path
BufferedReader bf = new BufferedReader(new InputStreamReader(aaptCmd.execute().in))
String line;
def begin = " $TAG_TITLE_BEGIN Begin Find UnDeflate Files $TAG_TITLE_END "
writeInfo(out, begin)
def count = 0
def totalSize = 0
while ((line = bf.readLine()) != null) {
if (line.contains('Stored')) {
def map = getAAPTLineInfo(line)
writeInfo(out, TAG_LIST_BEGIN + map['name'] + ' size:' + getUnitValue(map['size']) + TAG_LIST_END)
if (!map['name'].endsWith('resources.arsc')) {
count++;
totalSize += map['size']
}
}
}
def result = "$TAG_CONTENT_BEGIN find $TAG_SPECIAL_BEGIN" + count + " $TAG_SPECIAL_END files unDeflate(Except resources.arsc), total size: $TAG_SPECIAL_BEGIN " + getUnitValue(totalSize) + TAG_SPECIAL_END + TAG_CONTENT_END
writeInfo(out, result)
def tinyReduce = totalSize * 0.2

def tinyPNGAdvice = "$TAG_CONTENT_BEGIN if use the Tiny PNG(https://tinypng.com/),could reduce package size $TAG_SPECIAL_BEGIN " + getUnitValue(tinyReduce) + TAG_SPECIAL_END + TAG_CONTENT_END
writeInfo(out, tinyPNGAdvice)
def webpReduce = totalSize * 0.5

def webpAdvice = "$TAG_CONTENT_BEGIN if use webp(http://isparta.github.io/how.html | https://developers.google.com/speed/webp/docs/cwebp),could reduce package size $TAG_SPECIAL_BEGIN" + getUnitValue(webpReduce) + TAG_SPECIAL_END + TAG_CONTENT_END
writeInfo(out, webpAdvice)
def end = " $TAG_TITLE_BEGIN End Find UnDeflate Files $TAG_TITLE_END"

writeInfo(out, end)
bf.close()
}

//获取aapt分析的数据
def getAAPTLineInfo(String line) {
int length = 0;
int size = 0;
def lstr;
def sstr
try {
lstr = line.substring(0, 8).replaceAll("\\p{Space}", "")
sstr = line.substring(18, 26).replaceAll("\\p{Space}", "")
length = java.lang.Integer.valueOf(lstr)
size = java.lang.Integer.valueOf(sstr)
} catch (Exception e) {
print line + '\n'
print lstr + '<<<<<' + sstr + '\n'
}
String name = line.substring(68).replaceAll("\\p{Space}", "")
Map lineMap = ['length': length, 'size': size, 'name': name]
return lineMap
}

//获取无用资源文件信息
def getResourceUnuseFileInfo(String line) {
def oldLength = 0
def newLength = 0
try {
Pattern p1 = Pattern.compile(":(.*)bytes \\(replaced with");
Matcher m1 = p1.matcher(line);
while (m1.find()) {
def size = m1.group(1).replaceAll("\\p{Space}", "")
oldLength = Integer.valueOf(size)
}
Pattern p2 = Pattern.compile('size(.*)bytes');
Matcher m2 = p2.matcher(line);
while (m2.find()) {
def size = m2.group(1).replaceAll("\\p{Space}", "")
newLength = Integer.valueOf(size)
}
} catch (Exception e) {
print line
}

String name = line.substring(24)
Map lineMap = ['oldSize': oldLength, 'newSize': newLength, 'name': name]
return lineMap
}


def getReduceMinAndMax(def minPercent, def maxPercent) {
def size = getAPKSize()
def minValue = size * minPercent
def maxValue = size * maxPercent
def minStr = getUnitValue(minValue)
def maxStr = getUnitValue(maxValue)
return [min: minStr, max: maxStr]
}

//获取apk大小
def getAPKSize() {
File apkFile = getApkFile();
def size = apkFile.length()
}

//aapt
def aaptExe() {
def sdkPath = getSDKEnvPath()
def androidVersion = getBuildVersion()
def aaptWin = sdkPath + File.separator + 'build-tools' + File.separator + androidVersion + File.separator + 'aapt.exe'
def aaptUnit = sdkPath + File.separator + 'build-tools' + File.separator + androidVersion + File.separator + 'aapt'
File aaptWinFile = new File(aaptWin)
File aaptUnitFile = new File(aaptUnit)
if (aaptUnitFile.exists()) {
return aaptUnit
}

if (aaptWinFile.exists()) {
return aaptWin
}
}

//获取编译版本
def getBuildVersion() {
def version = project.android.buildToolsVersion
return version
}

//获取sdk位置
def getSDKEnvPath() {
def sdkPath;
File localPropertyFile;
localPropertyFile = project.rootProject.file('app/local.properties')
if (localPropertyFile.exists()) {
Properties properties = new Properties()
InputStream inputStream = localPropertyFile.newDataInputStream();
properties.load(inputStream)
sdkPath = properties.getProperty('sdk.dir')
}

if (sdkPath == null || sdkPath.length() < 3) {
localPropertyFile = project.rootProject.file('local.properties')
if (localPropertyFile.exists()) {
Properties properties = new Properties()
InputStream inputStream = localPropertyFile.newDataInputStream();
properties.load(inputStream)
sdkPath = properties.getProperty('sdk.dir')
}
}

return sdkPath;

}


def getUnitValue(def value) {
if (value < 1024) {
return " $value B"
} else {
value = value / 1024
}

if (value < 1024) {
return getTwoDecimalValue(value, ' KB')
} else {
value = value / 1024
}

if (value < 1024) {
return getTwoDecimalValue(value, ' MB')
} else {
value = value / 1024
return getTwoDecimalValue(value, ' GB')
}
}

def getTwoDecimalValue(def value, def unit) {
return String.format("%.1f", value) + unit
}

//写入报告文档
def writeInfo(def out, def info) {
if (out != null) {
out.write(info)
}
}
文章目录
  1. 1. 1、分析指标
  • 2、使用方法
  • 3、附projectfit.gradle源码
  • ,