/* * Copyright (C) 2016 ceabie (https://github.com/ceabie/) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.ceabie.dexknife; import org.gradle.api.Project; import org.gradle.api.file.FileTreeElement; import org.gradle.api.specs.NotSpec; import org.gradle.api.specs.OrSpec; import org.gradle.api.specs.Spec; import org.gradle.api.tasks.util.PatternSet; import java.io.*; import java.util.*; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; /** * the base of spilt tools. * * @author ceabie */ public class DexSplitTools { public static final String DEX_KNIFE_CFG_TXT = "dexknife.txt"; private static final String DEX_MINIMAL_MAIN_DEX = "--minimal-main-dex"; private static final String DEX_KNIFE_CFG_DEX_PARAM = "-dex-param"; private static final String DEX_KNIFE_CFG_SPLIT = "-split"; private static final String DEX_KNIFE_CFG_KEEP = "-keep"; private static final String DEX_KNIFE_CFG_AUTO_MAINDEX = "-auto-maindex"; private static final String DEX_KNIFE_CFG_DONOT_USE_SUGGEST = "-donot-use-suggest"; private static final String DEX_KNIFE_CFG_LOG_MAIN_DEX = "-log-mainlist"; private static final String DEX_KNIFE_CFG_FILTER_SUGGEST = "-filter-suggest"; private static final String DEX_KNIFE_CFG_SUGGEST_SPLIT = "-suggest-split"; private static final String DEX_KNIFE_CFG_SUGGEST_KEEP = "-suggest-keep"; private static final String DEX_KNIFE_CFG_LOG_FILTER_SUGGEST = "-log-filter-suggest"; private static final String DEX_KNIFE_CFG_LOG_FILTER = "-log-filter"; protected static final String MAINDEXLIST_TXT = "maindexlist.txt"; private static final String MAPPING_FLAG = " -> "; private static final int MAPPING_FLAG_LEN = MAPPING_FLAG.length(); private static final String CLASS_SUFFIX = ".class"; private static long StartTime = 0; protected static void startDexKnife() { System.out.println("DexKnife Processing ..."); StartTime = System.currentTimeMillis(); } protected static void endDexKnife() { String time; long internal = System.currentTimeMillis() - StartTime; if (internal > 1000) { float i = internal / 1000; if (i >= 60) { i = i / 60; int min = (int) i; time = min + " min " + (i - min) + " s"; } else { time = i + "s"; } } else { time = internal + "ms"; } System.out.println("DexKnife Finished: " + time); } public static boolean processMainDexList(Project project, boolean minifyEnabled, File mappingFile, File jarMergingOutputFile, File andMainDexList, DexKnifeConfig dexKnifeConfig) throws Exception { if (!minifyEnabled && jarMergingOutputFile == null) { System.out.println("DexKnife Error: jarMerging is Null! Skip DexKnife. Please report All Gradle Log."); return false; } try { return genMainDexList(project, minifyEnabled, mappingFile, jarMergingOutputFile, andMainDexList, dexKnifeConfig); } catch (Exception e) { e.printStackTrace(); } return false; } /** * get the config of dex knife */ protected static DexKnifeConfig getDexKnifeConfig(Project project) throws Exception { BufferedReader reader = new BufferedReader(new FileReader(project.file(DEX_KNIFE_CFG_TXT))); DexKnifeConfig dexKnifeConfig = new DexKnifeConfig(); String line; boolean matchCmd; boolean minimalMainDex = true; Set<String> addParams = new HashSet<>(); Set<String> splitToSecond = new HashSet<>(); Set<String> keepMain = new HashSet<>(); Set<String> splitSuggest = new HashSet<>(); Set<String> keepSuggest = new HashSet<>(); while ((line = reader.readLine()) != null) { line = line.trim(); if (line.length() == 0) { continue; } int rem = line.indexOf('#'); if (rem != -1) { if (rem == 0) { continue; } else { line = line.substring(0, rem).trim(); } } String cmd = line.toLowerCase(); matchCmd = true; if (DEX_KNIFE_CFG_AUTO_MAINDEX.equals(cmd)) { minimalMainDex = false; } else if (matchCommand(cmd, DEX_KNIFE_CFG_DEX_PARAM)) { String param = line.substring(DEX_KNIFE_CFG_DEX_PARAM.length()).trim(); if (!param.toLowerCase().startsWith("--main-dex-list")) { addParams.add(param); } } else if (matchCommand(cmd, DEX_KNIFE_CFG_SPLIT)) { String sPattern = line.substring(DEX_KNIFE_CFG_SPLIT.length()).trim(); addClassFilePath(sPattern, splitToSecond); } else if (matchCommand(cmd, DEX_KNIFE_CFG_KEEP)) { String sPattern = line.substring(DEX_KNIFE_CFG_KEEP.length()).trim(); addClassFilePath(sPattern, keepMain); } else if (DEX_KNIFE_CFG_DONOT_USE_SUGGEST.equals(cmd)) { dexKnifeConfig.useSuggest = false; } else if (DEX_KNIFE_CFG_FILTER_SUGGEST.equals(cmd)) { dexKnifeConfig.filterSuggest = true; } else if (DEX_KNIFE_CFG_LOG_MAIN_DEX.equals(cmd)) { dexKnifeConfig.logMainList = true; } else if (DEX_KNIFE_CFG_LOG_FILTER_SUGGEST.equals(cmd)) { dexKnifeConfig.logFilterSuggest = true; } else if (DEX_KNIFE_CFG_LOG_FILTER.equals(cmd)) { dexKnifeConfig.logFilter = true; } else if (matchCommand(cmd, DEX_KNIFE_CFG_SUGGEST_SPLIT)) { String sPattern = line.substring(DEX_KNIFE_CFG_SUGGEST_SPLIT.length()).trim(); addClassFilePath(sPattern, splitSuggest); } else if (matchCommand(cmd, DEX_KNIFE_CFG_SUGGEST_KEEP)) { String sPattern = line.substring(DEX_KNIFE_CFG_SUGGEST_KEEP.length()).trim(); addClassFilePath(sPattern, keepSuggest); } else if (!cmd.startsWith("-")) { addClassFilePath(line, splitToSecond); } else { matchCmd = false; } if (matchCmd) { System.out.println("DexKnife Config: " + line); } } reader.close(); if (minimalMainDex) { addParams.add(DEX_MINIMAL_MAIN_DEX); } // 使用ADT建议的mainlist if (dexKnifeConfig.useSuggest) { // 将全局过滤应用到建议的mainlist if (dexKnifeConfig.filterSuggest) { splitSuggest.addAll(splitToSecond); keepSuggest.addAll(keepMain); } if (!splitSuggest.isEmpty() || !keepSuggest.isEmpty()) { dexKnifeConfig.suggestPatternSet = new PatternSet() .exclude(splitSuggest) .include(keepSuggest); } } if (!splitToSecond.isEmpty() || !keepMain.isEmpty()) { dexKnifeConfig.patternSet = new PatternSet() .exclude(splitToSecond) .include(keepMain); } else { if (!dexKnifeConfig.useSuggest) { System.err.println("DexKnife Warning: NO SET split Or keep path, it will use Recommend!"); dexKnifeConfig.useSuggest = true; } } dexKnifeConfig.additionalParameters = addParams; return dexKnifeConfig; } private static boolean matchCommand(String text, String cmd) { Pattern pattern = Pattern.compile("^" + cmd + "\\s+"); return pattern.matcher(text).find(); } /** * add the class path to pattern list, and the single class pattern can work. */ private static void addClassFilePath(String classPath, Set<String> patternList) { if (classPath != null && classPath.length() > 0) { if (classPath.endsWith(CLASS_SUFFIX)) { classPath = classPath.substring(0, classPath.length() - CLASS_SUFFIX.length()) .replace('.', '/') + CLASS_SUFFIX; } else { classPath = classPath.replace('.', '/'); } patternList.add(classPath); } } private static Spec<FileTreeElement> getMaindexSpec(PatternSet patternSet) { Spec<FileTreeElement> maindexSpec = null; if (patternSet != null) { Spec<FileTreeElement> includeSpec = null; Spec<FileTreeElement> excludeSpec = null; if (!patternSet.getIncludes().isEmpty()) { includeSpec = patternSet.getAsIncludeSpec(); } if (!patternSet.getExcludes().isEmpty()) { excludeSpec = patternSet.getAsExcludeSpec(); } if (includeSpec != null && excludeSpec != null) { maindexSpec = new OrSpec<>(includeSpec, new NotSpec<>(excludeSpec)); } else { if (excludeSpec != null) { // only exclude maindexSpec = new NotSpec<>(excludeSpec); } else if (includeSpec != null) { // only include maindexSpec = includeSpec; } } } // if (maindexSpec == null) { // maindexSpec = Specs.satisfyAll(); // } return maindexSpec; } private static boolean isPatternSetEmpty(PatternSet patternSet) { return patternSet.getExcludes().isEmpty() && patternSet.getIncludes().isEmpty() && patternSet.getExcludeSpecs().isEmpty() && patternSet.getIncludeSpecs().isEmpty(); } /** * generate the main dex list */ private static boolean genMainDexList(Project project, boolean minifyEnabled, File mappingFile, File jarMergingOutputFile, File adtMainDexList, DexKnifeConfig dexKnifeConfig) throws Exception { System.out.println(":" + project.getName() + ":genMainDexList"); // 1.get the recommend adt's maindexlist Map<String, Boolean> recommendMainClasses = null; if (dexKnifeConfig.useSuggest && adtMainDexList != null && adtMainDexList.exists()) { PatternSet patternSet = dexKnifeConfig.suggestPatternSet; if (dexKnifeConfig.filterSuggest && patternSet == null) { patternSet = dexKnifeConfig.patternSet; } System.out.println("DexKnife: use suggest"); recommendMainClasses = getRecommendMainDexClasses(adtMainDexList, patternSet, dexKnifeConfig.logFilterSuggest); } File keepFile = project.file(MAINDEXLIST_TXT); keepFile.delete(); // 2.process the global filter List<String> mainClasses = null; if (dexKnifeConfig.patternSet == null || isPatternSetEmpty(dexKnifeConfig.patternSet)) { // only filter the suggest list if (recommendMainClasses != null && recommendMainClasses.size() > 0) { mainClasses = new ArrayList<>(); Set<Map.Entry<String, Boolean>> entries = recommendMainClasses.entrySet(); for (Map.Entry<String, Boolean> entry : entries) { if (entry.getValue()) { mainClasses.add(entry.getKey()); } } } } else { if (minifyEnabled) { System.err.println("DexKnife: From Mapping"); // get classes from mapping mainClasses = getMainClassesFromMapping(mappingFile, dexKnifeConfig.patternSet, recommendMainClasses, dexKnifeConfig.logFilter); } else { System.out.println("DexKnife: From MergedJar: " + jarMergingOutputFile); if (jarMergingOutputFile != null) { // get classes from merged jar mainClasses = getMainClassesFromJar(jarMergingOutputFile, dexKnifeConfig.patternSet, recommendMainClasses, dexKnifeConfig.logFilter); } else { System.err.println("DexKnife: The Merged Jar is not exist! Can't be processed!"); } } } // 3.create the miandexlist if (mainClasses != null && mainClasses.size() > 0) { BufferedWriter writer = new BufferedWriter(new FileWriter(keepFile)); for (String mainClass : mainClasses) { writer.write(mainClass); writer.newLine(); if (dexKnifeConfig.logMainList) { System.out.println(mainClass); } } writer.close(); return true; } throw new Exception("DexKnife Warning: Main dex is EMPTY ! Check your config and project!"); } /** * Gets main classes from jar. * * @param jarMergingOutputFile the jar merging output file * @param mainDexPattern the main dex pattern * @param adtMainCls the filter mapping of suggest classes * @param logFilter * @return the main classes from jar * @throws Exception the exception * @author ceabie */ private static ArrayList<String> getMainClassesFromJar( File jarMergingOutputFile, PatternSet mainDexPattern, Map<String, Boolean> adtMainCls, boolean logFilter) throws Exception { ZipFile clsFile = new ZipFile(jarMergingOutputFile); Spec<FileTreeElement> asSpec = getMaindexSpec(mainDexPattern); ClassFileTreeElement treeElement = new ClassFileTreeElement(); // lists classes from jar. ArrayList<String> mainDexList = new ArrayList<>(); Enumeration<? extends ZipEntry> entries = clsFile.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); String entryName = entry.getName(); if (entryName.endsWith(CLASS_SUFFIX)) { treeElement.setClassPath(entryName); if (isAtMainDex(adtMainCls, entryName, treeElement, asSpec, logFilter)) { mainDexList.add(entryName); } } } clsFile.close(); return mainDexList; } /** * Gets main classes from mapping. * * @param mapping the mapping file * @param mainDexPattern the main dex pattern * @param recommendMainCls the filter mapping of suggest classes * @param logFilter * @return the main classes from mapping * @throws Exception the exception * @author ceabie */ private static List<String> getMainClassesFromMapping( File mapping, PatternSet mainDexPattern, Map<String, Boolean> recommendMainCls, boolean logFilter) throws Exception { String line; List<String> mainDexList = new ArrayList<>(); BufferedReader reader = new BufferedReader(new FileReader(mapping)); // all classes ClassFileTreeElement filterElement = new ClassFileTreeElement(); Spec<FileTreeElement> asSpec = getMaindexSpec(mainDexPattern); while ((line = reader.readLine()) != null) { line = line.trim(); if (line.endsWith(":")) { int flagPos = line.indexOf(MAPPING_FLAG); if (flagPos != -1) { String sOrgCls = line.substring(0, flagPos).replace('.', '/') + CLASS_SUFFIX; String sMapCls = line.substring(flagPos + MAPPING_FLAG_LEN, line.length() - 1) .replace('.', '/') + CLASS_SUFFIX; filterElement.setClassPath(sOrgCls); if (isAtMainDex(recommendMainCls, sMapCls, filterElement, asSpec, logFilter)) { mainDexList.add(sMapCls); } } } } reader.close(); return mainDexList; } private static boolean isAtMainDex( Map<String, Boolean> mainCls, String sMapCls, ClassFileTreeElement treeElement, Spec<FileTreeElement> asSpec, boolean logFilter) { boolean isRecommend = false; // adt推荐 if (mainCls != null) { Boolean value = mainCls.get(sMapCls); if (value != null) { isRecommend = value; } } // 全局过滤 boolean inGlobalFilter = asSpec != null && asSpec.isSatisfiedBy(treeElement); if (logFilter) { String ret; if (isRecommend && inGlobalFilter) { ret = "true"; } else if (isRecommend) { ret = "Recommend"; } else if (inGlobalFilter) { ret = "Global"; } else { ret = "false"; } String s = "AtMainDex: " + treeElement.getPath() + " [" + ret + "]"; if (isRecommend || inGlobalFilter) { System.err.println(s); } else { System.out.println(s); } } // 合并结果 return isRecommend || inGlobalFilter; } /** * get the maindexlist of android gradle plugin. * if enable ProGuard, return the mapped class. */ private static Map<String, Boolean> getRecommendMainDexClasses( File adtMainDexList, PatternSet mainDexPattern, boolean logFilter) throws Exception { if (adtMainDexList == null || !adtMainDexList.exists()) { System.err.println("DexKnife Warning: Android recommend Main dex is no exist."); return null; } HashMap<String, Boolean> mainCls = new HashMap<>(); BufferedReader reader = new BufferedReader(new FileReader(adtMainDexList)); ClassFileTreeElement treeElement = new ClassFileTreeElement(); Spec<FileTreeElement> asSpec = mainDexPattern != null ? getMaindexSpec(mainDexPattern) : null; String line, clsPath; while ((line = reader.readLine()) != null) { line = line.trim(); int clsPos = line.lastIndexOf(CLASS_SUFFIX); if (clsPos != -1) { boolean satisfiedBy = true; if (asSpec != null) { clsPath = line.substring(0, clsPos).replace('.', '/') + CLASS_SUFFIX; treeElement.setClassPath(clsPath); satisfiedBy = asSpec.isSatisfiedBy(treeElement); if (logFilter) { System.out.println("DexKnife-Suggest: [" + (satisfiedBy ? "Keep" : "Split") + "] " + clsPath); } } mainCls.put(line, satisfiedBy); } } reader.close(); if (mainCls.size() == 0) { mainCls = null; } return mainCls; } static int getAndroidPluginVersion(String version) { int size = version.length(); int ver = 0; for (int i = 0; i < size; i++) { char c = version.charAt(i); if (Character.isDigit(c) || c == '.') { if (c != '.') { ver = ver * 10 + c - '0'; } } else { break; } } return ver; } }