前言
Android Studio提供了一套静态代码分析工具。它可以检查出:xml文件中 unused resources没有使用到的资源等等。
这篇文章的主题是APK Reduce Size。
所以第一步,用Lint来 Check unused resources。
第二步,我们利用第一步的结果,java 实现 batch clean unused resource。
这里,我写了一个Demo,可以直接拿过来用
https://github.com/AlvinScrp/PerformanceOptimizationCase/tree/master/reducesizecase
Check unused resources
Android Lint运行
Android studio –>Analyze–>Inspect Code,根据指示确定就好。
Lint 结果
Inspection –> Android>Lint>Performance–> Unused resources,这里列出了Demo中的Unused Resources,可以双击跳转到指定位置删除,
也可以复制到文件,批量处理。如下图。
批量处理 unused resource
处理之前,我们有必要知道下资源文件的结构
有的资源有xml File形式drawable,layout,mipmap,menu,anim
有的资源有行key-value形式string,dimen,color
我们通过上一步得到一个文件,下面处理的
clean的java代码实现地址:
https://github.com/AlvinScrp/PerformanceOptimizationCase/tree/master/unusedResourcesCleanTool
处理流程大致就是文件操作:
1.文件先转成一个Map,
2.两层遍历res 目录和Map,根据xml File/key-value 删除resource
PS:在使用之前需要配置你的sourceProjectResDir和lintResultFilePath
你可以把代码拿过去打成jar文件
package com.example;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class UnusedResourcesClean {
static String Type_drawable = "drawable";
static String Type_layout = "layout";
static String Type_string = "string";
static String Type_color = "color";
static String Type_dimen = "dimen";
static String Type_menu = "menu";
static String Type_anim = "anim";
private static boolean cleanDrawable = true;
private static boolean cleanLayout = true;
private static boolean cleanAnim = true;
private static boolean cleanMenu = true;
private static boolean cleanColor = true;
private static boolean cleanString = true;
private static boolean cleanDimen = true;
/**
* project res dir
*/
static String sourceProjectResDir = "/Users/alvin/Documents/workproject/PerformanceOptimizationCase/reduceSizeCase/src/main/res";
/**
* lint check result file path
*/
static String lintResultFilePath = "unusedResourcesCleanTool/unusedLint";
/**
* key: resourceType
* value: resource file,or resource key name
*/
static HashMap<String, Set<String>> resMap = new HashMap<>();
public static void main(String[] args) {
try {
resMap = initUnUsedMap(lintResultFilePath);
if (resMap == null || resMap.isEmpty()) {
return;
}
printUnUsedMap();
clean(sourceProjectResDir,resMap);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* split The resource 'R.drawable.btn_bg_top_normal' appears to be unused
* lint result --> map
*
* @param resultFilePath
*/
private static HashMap<String, Set<String>> initUnUsedMap(String resultFilePath) throws Exception {
HashMap<String, Set<String>> resMap = new HashMap<>();
try {
BufferedReader bf = new BufferedReader(new FileReader(resultFilePath));
String line = "";
while ((line = bf.readLine()) != null) { //
if (line.contains("appears to be unused")) {
String resKey = line.split("'")[1];//R.drawable.btn_bg_top_normal
System.out.println(resKey);
String resType = resKey.split("\\.")[1];
String resName = resKey.split("\\.")[2];
System.out.println(resType + " , " + resName);
if (resMap.get(resType) == null) {
resMap.put(resType, new HashSet<String>());
}
Set<String> ids = resMap.get(resType);
ids.add(resName);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return resMap;
}
private static void printUnUsedMap() {
for (Map.Entry<String, Set<String>> entry : resMap.entrySet()) {
System.out.println("------------------" + entry.getKey() + "------------------");
for (String s : entry.getValue()) {
System.out.println(s);
}
System.out.println(" ");
}
}
/**
*
* @param sourceProjectResDir 项目资源目录
* @param resMap 根据Type分类的unusedResource
* 例如 key: string value: {rateuslib_cancel,unnecessary_cancel}
*/
private static void clean(String sourceProjectResDir,HashMap<String, Set<String>> resMap) {
Map<String, Boolean> deletionFileMap = new HashMap<>();
deletionFileMap.put(Type_drawable, cleanDrawable);
deletionFileMap.put(Type_layout, cleanLayout);
deletionFileMap.put(Type_anim, cleanAnim);
deletionFileMap.put(Type_menu, cleanMenu);
deletionFileMap.put(Type_color, cleanColor);
Map<String, Boolean> deletionLineMap = new HashMap<>();
deletionLineMap.put(Type_string, cleanString);
deletionLineMap.put(Type_dimen, cleanDimen);
deletionLineMap.put(Type_color, cleanColor);
Map<String, String> lineKeyMap = new HashMap<>();
lineKeyMap.put(Type_string, "<string name");
lineKeyMap.put(Type_dimen, "<dimen name=");
lineKeyMap.put(Type_color, "<color name=");
File dir = new File(sourceProjectResDir);
for (File file : dir.listFiles()) {
if (!file.isDirectory()) {
continue;
}
if (file.getName().contains("value")) {
for (File file1 : file.listFiles()) {
for (Map.Entry<String, Boolean> entry : deletionLineMap.entrySet()) {
String type = entry.getKey();
boolean clean = entry.getValue();
if (file1.getName().contains(type) && clean) {
resetXml(file1, resMap.get(type), lineKeyMap.get(type));
}
}
}
} else {
for (Map.Entry<String, Boolean> entry : deletionFileMap.entrySet()) {
if (file.getName().contains(entry.getKey()) && entry.getValue()) {
Set<String> resSet = resMap.get(entry.getKey());
deleteResFile(resSet, file);
}
}
}
}
}
/**
* 从指定资源目录中,删除指定类型的文件
*
* @param resourceFiles
* @param resDir drawable/ or layout/ ...
*/
private static void deleteResFile(Set<String> resourceFiles, File resDir) {
try {
for (File file1 : resDir.listFiles()) {
if (resourceFiles.contains(file1.getName().split("\\.")[0])) {
file1.delete();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* @param sourceFile 需要删除行的文件
* @param unusedSourceKeys 需要删除的行的关键词集合
* @param xmlTypeKey "<string name" or "<dimen name=" or "<color name="
*/
public static void resetXml(File sourceFile, Set<String> unusedSourceKeys, String xmlTypeKey) {
BufferedWriter bw = null;
try {
if (unusedSourceKeys==null&&unusedSourceKeys.isEmpty()){
return;
}
String outFilename = sourceFile.getAbsolutePath() + "_temp";
File outFile = new File(outFilename);
BufferedReader bf = new BufferedReader(new FileReader(sourceFile));
bw = new BufferedWriter(new FileWriter(outFilename));
String line = "";// <string name="app_name">App Backup & Restore</string>
while ((line = bf.readLine()) != null) {
if (line.contains(xmlTypeKey) && unusedSourceKeys.contains(line.split("\"")[1])) {
continue;
}
bw.write(line);
bw.newLine();
}
sourceFile.delete();
outFile.renameTo(new File(sourceFile.getAbsolutePath()));
} catch (Exception e) {
} finally {
if (bw != null) {
try {
bw.flush();
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}