/* * 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.wj.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.specs.Specs; import org.gradle.api.tasks.util.PatternSet; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashSet; import java.util.Set; 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 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; } return genMainDexList(project, minifyEnabled, mappingFile, jarMergingOutputFile, andMainDexList, dexKnifeConfig); } /** * get the config of dex knife * * @param project project * @return DexKnifeConfig * @throws Exception exception */ 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 (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); } if (dexKnifeConfig.useSuggest) { if (dexKnifeConfig.filterSuggest) { splitSuggest.addAll(splitToSecond); keepSuggest.addAll(keepMain); } // for (String s : splitSuggest) { // System.out.println("Suggest: " + s); // } if (!splitSuggest.isEmpty() || !keepSuggest.isEmpty()) { dexKnifeConfig.suggestPatternSet = new PatternSet() .exclude(splitSuggest) .include(keepSuggest); } } if (!splitToSecond.isEmpty() || !keepMain.isEmpty()) { // for (String s : splitToSecond) { // System.out.println(s); // } dexKnifeConfig.patternSet = new PatternSet() .exclude(splitToSecond) .include(keepMain); } else { dexKnifeConfig.useSuggest = true; System.err.println("DexKnife Warning: NO SET split Or keep path, it will use Suggest!"); } 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; } /** * generate the main dex list */ private static boolean genMainDexList(Project project, boolean minifyEnabled, File mappingFile, File jarMergingOutputFile, File andMainDexList, DexKnifeConfig dexKnifeConfig) throws Exception { System.out.println(":" + project.getName() + ":genMainDexList"); // get the adt's maindexlist HashSet<String> mainCls = null; if (dexKnifeConfig.useSuggest) { PatternSet patternSet = dexKnifeConfig.suggestPatternSet; if (dexKnifeConfig.filterSuggest && patternSet == null) { patternSet = dexKnifeConfig.patternSet; } mainCls = getAdtMainDexClasses(andMainDexList, patternSet, dexKnifeConfig.logFilterSuggest); System.out.println("DexKnife: use suggest"); } File keepFile = project.file(MAINDEXLIST_TXT); keepFile.delete(); ArrayList<String> mainClasses = null; if (minifyEnabled) { System.err.println("DexKnife: From Mapping"); // get classes from mapping mainClasses = getMainClassesFromMapping(mappingFile, dexKnifeConfig.patternSet, mainCls); } else { System.out.println("DexKnife: From MergedJar: " + jarMergingOutputFile); if (jarMergingOutputFile != null) { // get classes from merged jar mainClasses = getMainClassesFromJar(jarMergingOutputFile, dexKnifeConfig.patternSet, mainCls); } else { System.err.println("DexKnife: The Merged Jar is not exist! Can't be processed!"); } } 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!"); } private static ArrayList<String> getMainClassesFromJar( File jarMergingOutputFile, PatternSet mainDexPattern, HashSet<String> mainCls) throws Exception { ZipFile clsFile = new ZipFile(jarMergingOutputFile); Spec<FileTreeElement> asSpec = getMaindexSpec(mainDexPattern); ClassFileTreeElement treeElement = new ClassFileTreeElement(); 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 (asSpec.isSatisfiedBy(treeElement) || (mainCls != null && mainCls.contains(entryName))) { mainDexList.add(entryName); } } } clsFile.close(); return mainDexList; } /** * Gets main classes from mapping. * * @param mapping the mapping file * @param mainDexPattern the main dex pattern * @param mainCls the main cls * @return the main classes from mapping * @throws Exception the exception * @author ceabie */ private static ArrayList<String> getMainClassesFromMapping( File mapping, PatternSet mainDexPattern, HashSet<String> mainCls) throws Exception { String line; ArrayList<String> mainDexList = new ArrayList<>(); BufferedReader reader = new BufferedReader(new FileReader(mapping)); ClassFileTreeElement treeElement = 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 sOrg = line.substring(0, flagPos).replace('.', '/') + CLASS_SUFFIX; treeElement.setClassPath(sOrg); if (asSpec.isSatisfiedBy(treeElement) || (mainCls != null && mainCls.contains(sOrg))) { String sMap = line.substring(flagPos + MAPPING_FLAG_LEN, line.length() - 1) .replace('.', '/') + CLASS_SUFFIX; mainDexList.add(sMap); } } } } reader.close(); return mainDexList; } /** * get the maindexlist of android gradle plugin */ private static HashSet<String> getAdtMainDexClasses(File outputDir, PatternSet mainDexPattern, boolean logFilter) throws Exception { if (outputDir == null || !outputDir.exists()) { System.err.println( "DexKnife Warning: Android recommand Main dex is no exist, try run again!"); return null; } HashSet<String> mainCls = new HashSet<>(); BufferedReader reader = new BufferedReader(new FileReader(outputDir)); 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) { if (asSpec != null) { clsPath = line.substring(0, clsPos).replace('.', '/') + CLASS_SUFFIX; treeElement.setClassPath(clsPath); boolean satisfiedBy = asSpec.isSatisfiedBy(treeElement); if (!satisfiedBy) { if (logFilter) System.out.println("DexKnife-Suggest: [Split] " + clsPath); continue; } if (logFilter) System.out.println("DexKnife-Suggest: [Keep] " + clsPath); } mainCls.add(line); } } 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; } }