由于工作的需要看了下Eclipse下android65535的解决方案,查了好多文档,真心的发自内心的说一句请不要再拷贝别人的博客了,害人,真害人。
接下来我说下我的实现方式,首先说下65535的最可能的触发原因(三方jar用的太多了)
首先:合并jar.
这里合并到jar使用的事ant的脚本,如何你电脑安装了ant,那ok,如果没有安装这里也不啰嗦告诉你怎么安装了,百度一下吧,安装总的来说没啥技术含量。安装ant之后配置如下脚本文件。
<?xml version="1.0" encoding="utf-8"?> <project name="b" basedir="E:\libs\all" default="makeSuperJar"> <target name="makeSuperJar" description="description"> <jar destfile="all.jar"> <zipfileset src="Android_Location_V1.1.2.jar"/> <zipfileset src="Android_Map_2.1.4.jar"/> <zipfileset src="Android_Services_2.1.4.jar"/> <zipfileset src="commons-net-3.3.jar"/> <zipfileset src="gson-2.2.1.jar"/> </jar> </target> </project>
这里你只需要改下你的basedir目录地址,destfile输出文件的名字和zipfileset你需要合并的jar即可。
然后将合并的jar转换成dex文件,怎么找到dx工具,见图
直接在当前路径下执行cmd命令,然后输入dx --dex --output=E:\libs\classes.dex E:\libs\all.jar,这里我写的是我自己的路径。输出文件为classes.dex,由于apk默认会将项目中的class文件编译成classes.dex,所以这里你需要更改下你的输出文件名,这里这个名字要有规范,严格的命名classes2.dex,classes3.dex.....,至于为什么,这是MultiDex的自己要求的,这里是仿Android sutudio的分包方式,请严格执行。
之后将classes.dex文件放置到项目的src目录下即可。
现在执行你还差最后一步导入MutiDex类库,你可以在网上去下载,或者直接copy我下边的代码,这里最主要想说的是如何使用
在你的项目的Application类中配置如下代码:
1 public class MyApplication extends Application{ 2 3 @Override 4 protected void attachBaseContext(Context base) { 5 // TODO Auto-generated method stub 6 super.attachBaseContext(base); 7 MultiDex.install(this); 8 9 } 10 }
到这里基本上配置算是完成了,this all over.
一下是类库MultiDex的类库Code,有需要的直接拷贝即可,这里不作为关键点来分析
MultiDex类:
/* * Copyright (C) 2013 The Android Open Source Project * * 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 android.support.multidex; import android.app.Application; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Build; import android.util.Log; import dalvik.system.DexFile; import java.io.File; import java.io.IOException; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.ListIterator; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipFile; /** * Monkey patches {@link Context#getClassLoader() the application context class * loader} in order to load classes from more than one dex file. The primary * {@code classes.dex} must contain the classes necessary for calling this * class methods. Secondary dex files named classes2.dex, classes3.dex... found * in the application apk will be added to the classloader after first call to * {@link #install(Context)}. * * <p/> * This library provides compatibility for platforms with API level 4 through 20. This library does * nothing on newer versions of the platform which provide built-in support for secondary dex files. */ public final class MultiDex { static final String TAG = "MultiDex"; private static final String OLD_SECONDARY_FOLDER_NAME = "secondary-dexes"; private static final String SECONDARY_FOLDER_NAME = "code_cache" + File.separator + "secondary-dexes"; private static final int MAX_SUPPORTED_SDK_VERSION = 20; private static final int MIN_SDK_VERSION = 4; private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2; private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1; private static final Set<String> installedApk = new HashSet<String>(); private static final boolean IS_VM_MULTIDEX_CAPABLE = isVMMultidexCapable(System.getProperty("java.vm.version")); private MultiDex() {} /** * Patches the application context class loader by appending extra dex files * loaded from the application apk. This method should be called in the * attachBaseContext of your {@link Application}, see * {@link MultiDexApplication} for more explanation and an example. * * @param context application context. * @throws RuntimeException if an error occurred preventing the classloader * extension. */ public static void install(Context context) { Log.i(TAG, "install"); if (IS_VM_MULTIDEX_CAPABLE) { Log.i(TAG, "VM has multidex support, MultiDex support library is disabled."); return; } if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) { throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + "."); } try { ApplicationInfo applicationInfo = getApplicationInfo(context); if (applicationInfo == null) { // Looks like running on a test Context, so just return without patching. return; } synchronized (installedApk) { String apkPath = applicationInfo.sourceDir; if (installedApk.contains(apkPath)) { return; } installedApk.add(apkPath); if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) { Log.w(TAG, "MultiDex is not guaranteed to work in SDK version " + Build.VERSION.SDK_INT + ": SDK version higher than " + MAX_SUPPORTED_SDK_VERSION + " should be backed by " + "runtime with built-in multidex capabilty but it‘s not the " + "case here: java.vm.version=\"" + System.getProperty("java.vm.version") + "\""); } /* The patched class loader is expected to be a descendant of * dalvik.system.BaseDexClassLoader. We modify its * dalvik.system.DexPathList pathList field to append additional DEX * file entries. */ ClassLoader loader; try { loader = context.getClassLoader(); } catch (RuntimeException e) { /* Ignore those exceptions so that we don‘t break tests relying on Context like * a android.test.mock.MockContext or a android.content.ContextWrapper with a * null base Context. */ Log.w(TAG, "Failure while trying to obtain Context class loader. " + "Must be running in test mode. Skip patching.", e); return; } if (loader == null) { // Note, the context class loader is null when running Robolectric tests. Log.e(TAG, "Context class loader is null. Must be running in test mode. " + "Skip patching."); return; } try { clearOldDexDir(context); } catch (Throwable t) { Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, " + "continuing without cleaning.", t); } File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME); List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false); if (checkValidZipFiles(files)) { installSecondaryDexes(loader, dexDir, files); } else { Log.w(TAG, "Files were not valid zip files. Forcing a reload."); // Try again, but this time force a reload of the zip file. files = MultiDexExtractor.load(context, applicationInfo, dexDir, true); if (checkValidZipFiles(files)) { installSecondaryDexes(loader, dexDir, files); } else { // Second time didn‘t work, give up throw new RuntimeException("Zip files were not valid."); } } } } catch (Exception e) { Log.e(TAG, "Multidex installation failure", e); throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ")."); } Log.i(TAG, "install done"); } private static ApplicationInfo getApplicationInfo(Context context) throws NameNotFoundException { PackageManager pm; String packageName; try { pm = context.getPackageManager(); packageName = context.getPackageName(); } catch (RuntimeException e) { /* Ignore those exceptions so that we don‘t break tests relying on Context like * a android.test.mock.MockContext or a android.content.ContextWrapper with a null * base Context. */ Log.w(TAG, "Failure while trying to obtain ApplicationInfo from Context. " + "Must be running in test mode. Skip patching.", e); return null; } if (pm == null || packageName == null) { // This is most likely a mock context, so just return without patching. return null; } ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA); return applicationInfo; } /** * Identifies if the current VM has a native support for multidex, meaning there is no need for * additional installation by this library. * @return true if the VM handles multidex */ /* package visible for test */ static boolean isVMMultidexCapable(String versionString) { boolean isMultidexCapable = false; if (versionString != null) { Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString); if (matcher.matches()) { try { int major = Integer.parseInt(matcher.group(1)); int minor = Integer.parseInt(matcher.group(2)); isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR) || ((major == VM_WITH_MULTIDEX_VERSION_MAJOR) && (minor >= VM_WITH_MULTIDEX_VERSION_MINOR)); } catch (NumberFormatException e) { // let isMultidexCapable be false } } } Log.i(TAG, "VM with version " + versionString + (isMultidexCapable ? " has multidex support" : " does not have multidex support")); return isMultidexCapable; } private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException { if (!files.isEmpty()) { if (Build.VERSION.SDK_INT >= 19) { V19.install(loader, files, dexDir); } else if (Build.VERSION.SDK_INT >= 14) { V14.install(loader, files, dexDir); } else { V4.install(loader, files); } } } /** * Returns whether all files in the list are valid zip files. If {@code files} is empty, then * returns true. */ private static boolean checkValidZipFiles(List<File> files) { for (File file : files) { if (!MultiDexExtractor.verifyZipFile(file)) { return false; } } return true; } /** * Locates a given field anywhere in the class inheritance hierarchy. * * @param instance an object to search the field into. * @param name field name * @return a field object * @throws NoSuchFieldException if the field cannot be located */ private static Field findField(Object instance, String name) throws NoSuchFieldException { for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) { try { Field field = clazz.getDeclaredField(name); if (!field.isAccessible()) { field.setAccessible(true); } return field; } catch (NoSuchFieldException e) { // ignore and search next } } throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass()); } /** * Locates a given method anywhere in the class inheritance hierarchy. * * @param instance an object to search the method into. * @param name method name * @param parameterTypes method parameter types * @return a method object * @throws NoSuchMethodException if the method cannot be located */ private static Method findMethod(Object instance, String name, Class<?>... parameterTypes) throws NoSuchMethodException { for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) { try { Method method = clazz.getDeclaredMethod(name, parameterTypes); if (!method.isAccessible()) { method.setAccessible(true); } return method; } catch (NoSuchMethodException e) { // ignore and search next } } throw new NoSuchMethodException("Method " + name + " with parameters " + Arrays.asList(parameterTypes) + " not found in " + instance.getClass()); } /** * Replace the value of a field containing a non null array, by a new array containing the * elements of the original array plus the elements of extraElements. * @param instance the instance whose field is to be modified. * @param fieldName the field to modify. * @param extraElements elements to append at the end of the array. */ private static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Field jlrField = findField(instance, fieldName); Object[] original = (Object[]) jlrField.get(instance); Object[] combined = (Object[]) Array.newInstance( original.getClass().getComponentType(), original.length + extraElements.length); System.arraycopy(original, 0, combined, 0, original.length); System.arraycopy(extraElements, 0, combined, original.length, extraElements.length); jlrField.set(instance, combined); } private static void clearOldDexDir(Context context) throws Exception { File dexDir = new File(context.getFilesDir(), OLD_SECONDARY_FOLDER_NAME); if (dexDir.isDirectory()) { Log.i(TAG, "Clearing old secondary dex dir (" + dexDir.getPath() + ")."); File[] files = dexDir.listFiles(); if (files == null) { Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ")."); return; } for (File oldFile : files) { Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " + oldFile.length()); if (!oldFile.delete()) { Log.w(TAG, "Failed to delete old file " + oldFile.getPath()); } else { Log.i(TAG, "Deleted old file " + oldFile.getPath()); } } if (!dexDir.delete()) { Log.w(TAG, "Failed to delete secondary dex dir " + dexDir.getPath()); } else { Log.i(TAG, "Deleted old secondary dex dir " + dexDir.getPath()); } } } /** * Installer for platform versions 19. */ private static final class V19 { private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException { /* The patched class loader is expected to be a descendant of * dalvik.system.BaseDexClassLoader. We modify its * dalvik.system.DexPathList pathList field to append additional DEX * file entries. */ Field pathListField = findField(loader, "pathList"); Object dexPathList = pathListField.get(loader); ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList<File>(additionalClassPathEntries), optimizedDirectory, suppressedExceptions)); if (suppressedExceptions.size() > 0) { for (IOException e : suppressedExceptions) { Log.w(TAG, "Exception in makeDexElement", e); } Field suppressedExceptionsField = findField(loader, "dexElementsSuppressedExceptions"); IOException[] dexElementsSuppressedExceptions = (IOException[]) suppressedExceptionsField.get(loader); if (dexElementsSuppressedExceptions == null) { dexElementsSuppressedExceptions = suppressedExceptions.toArray( new IOException[suppressedExceptions.size()]); } else { IOException[] combined = new IOException[suppressedExceptions.size() + dexElementsSuppressedExceptions.length]; suppressedExceptions.toArray(combined); System.arraycopy(dexElementsSuppressedExceptions, 0, combined, suppressedExceptions.size(), dexElementsSuppressedExceptions.length); dexElementsSuppressedExceptions = combined; } suppressedExceptionsField.set(loader, dexElementsSuppressedExceptions); } } /** * A wrapper around * {@code private static final dalvik.system.DexPathList#makeDexElements}. */ private static Object[] makeDexElements( Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { Method makeDexElements = findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class); return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions); } } /** * Installer for platform versions 14, 15, 16, 17 and 18. */ private static final class V14 { private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException { /* The patched class loader is expected to be a descendant of * dalvik.system.BaseDexClassLoader. We modify its * dalvik.system.DexPathList pathList field to append additional DEX * file entries. */ Field pathListField = findField(loader, "pathList"); Object dexPathList = pathListField.get(loader); expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList<File>(additionalClassPathEntries), optimizedDirectory)); } /** * A wrapper around * {@code private static final dalvik.system.DexPathList#makeDexElements}. */ private static Object[] makeDexElements( Object dexPathList, ArrayList<File> files, File optimizedDirectory) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { Method makeDexElements = findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class); return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory); } } /** * Installer for platform versions 4 to 13. */ private static final class V4 { private static void install(ClassLoader loader, List<File> additionalClassPathEntries) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, IOException { /* The patched class loader is expected to be a descendant of * dalvik.system.DexClassLoader. We modify its * fields mPaths, mFiles, mZips and mDexs to append additional DEX * file entries. */ int extraSize = additionalClassPathEntries.size(); Field pathField = findField(loader, "path"); StringBuilder path = new StringBuilder((String) pathField.get(loader)); String[] extraPaths = new String[extraSize]; File[] extraFiles = new File[extraSize]; ZipFile[] extraZips = new ZipFile[extraSize]; DexFile[] extraDexs = new DexFile[extraSize]; for (ListIterator<File> iterator = additionalClassPathEntries.listIterator(); iterator.hasNext();) { File additionalEntry = iterator.next(); String entryPath = additionalEntry.getAbsolutePath(); path.append(‘:‘).append(entryPath); int index = iterator.previousIndex(); extraPaths[index] = entryPath; extraFiles[index] = additionalEntry; extraZips[index] = new ZipFile(additionalEntry); extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0); } pathField.set(loader, path.toString()); expandFieldArray(loader, "mPaths", extraPaths); expandFieldArray(loader, "mFiles", extraFiles); expandFieldArray(loader, "mZips", extraZips); expandFieldArray(loader, "mDexs", extraDexs); } } }
MultiDexApplication类
1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.support.multidex; 18 19 import android.app.Application; 20 import android.content.Context; 21 22 /** 23 * Minimal MultiDex capable application. To use the legacy multidex library there is 3 possibility: 24 * <ul> 25 * <li>Declare this class as the application in your AndroidManifest.xml.</li> 26 * <li>Have your {@link Application} extends this class.</li> 27 * <li>Have your {@link Application} override attachBaseContext starting with<br> 28 * <code> 29 protected void attachBaseContext(Context base) {<br> 30 super.attachBaseContext(base);<br> 31 MultiDex.install(this); 32 </code></li> 33 * <ul> 34 */ 35 public class MultiDexApplication extends Application { 36 @Override 37 protected void attachBaseContext(Context base) { 38 super.attachBaseContext(base); 39 MultiDex.install(this); 40 } 41 }
MultiDexExtractor类
1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.support.multidex; 18 19 import android.content.Context; 20 import android.content.SharedPreferences; 21 import android.content.pm.ApplicationInfo; 22 import android.os.Build; 23 import android.util.Log; 24 25 import java.io.BufferedOutputStream; 26 import java.io.Closeable; 27 import java.io.File; 28 import java.io.FileFilter; 29 import java.io.FileNotFoundException; 30 import java.io.FileOutputStream; 31 import java.io.IOException; 32 import java.io.InputStream; 33 import java.lang.reflect.InvocationTargetException; 34 import java.lang.reflect.Method; 35 import java.util.ArrayList; 36 import java.util.List; 37 import java.util.zip.ZipEntry; 38 import java.util.zip.ZipException; 39 import java.util.zip.ZipFile; 40 import java.util.zip.ZipOutputStream; 41 42 /** 43 * Exposes application secondary dex files as files in the application data 44 * directory. 45 */ 46 final class MultiDexExtractor { 47 48 private static final String TAG = MultiDex.TAG; 49 50 /** 51 * We look for additional dex files named {@code classes2.dex}, 52 * {@code classes3.dex}, etc. 53 */ 54 private static final String DEX_PREFIX = "classes"; 55 private static final String DEX_SUFFIX = ".dex"; 56 57 private static final String EXTRACTED_NAME_EXT = ".classes"; 58 private static final String EXTRACTED_SUFFIX = ".zip"; 59 private static final int MAX_EXTRACT_ATTEMPTS = 3; 60 61 private static final String PREFS_FILE = "multidex.version"; 62 private static final String KEY_TIME_STAMP = "timestamp"; 63 private static final String KEY_CRC = "crc"; 64 private static final String KEY_DEX_NUMBER = "dex.number"; 65 66 /** 67 * Size of reading buffers. 68 */ 69 private static final int BUFFER_SIZE = 0x4000; 70 /* Keep value away from 0 because it is a too probable time stamp value */ 71 private static final long NO_VALUE = -1L; 72 73 /** 74 * Extracts application secondary dexes into files in the application data 75 * directory. 76 * 77 * @return a list of files that were created. The list may be empty if there 78 * are no secondary dex files. 79 * @throws IOException if encounters a problem while reading or writing 80 * secondary dex files 81 */ 82 static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir, 83 boolean forceReload) throws IOException { 84 Log.i(TAG, "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")"); 85 final File sourceApk = new File(applicationInfo.sourceDir); 86 87 long currentCrc = getZipCrc(sourceApk); 88 89 List<File> files; 90 if (!forceReload && !isModified(context, sourceApk, currentCrc)) { 91 try { 92 files = loadExistingExtractions(context, sourceApk, dexDir); 93 } catch (IOException ioe) { 94 Log.w(TAG, "Failed to reload existing extracted secondary dex files," 95 + " falling back to fresh extraction", ioe); 96 files = performExtractions(sourceApk, dexDir); 97 putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1); 98 99 } 100 } else { 101 Log.i(TAG, "Detected that extraction must be performed."); 102 files = performExtractions(sourceApk, dexDir); 103 putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1); 104 } 105 106 Log.i(TAG, "load found " + files.size() + " secondary dex files"); 107 return files; 108 } 109 110 private static List<File> loadExistingExtractions(Context context, File sourceApk, File dexDir) 111 throws IOException { 112 Log.i(TAG, "loading existing secondary dex files"); 113 114 final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT; 115 int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1); 116 final List<File> files = new ArrayList<File>(totalDexNumber); 117 118 for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) { 119 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX; 120 File extractedFile = new File(dexDir, fileName); 121 if (extractedFile.isFile()) { 122 files.add(extractedFile); 123 if (!verifyZipFile(extractedFile)) { 124 Log.i(TAG, "Invalid zip file: " + extractedFile); 125 throw new IOException("Invalid ZIP file."); 126 } 127 } else { 128 throw new IOException("Missing extracted secondary dex file ‘" + 129 extractedFile.getPath() + "‘"); 130 } 131 } 132 133 return files; 134 } 135 136 private static boolean isModified(Context context, File archive, long currentCrc) { 137 SharedPreferences prefs = getMultiDexPreferences(context); 138 return (prefs.getLong(KEY_TIME_STAMP, NO_VALUE) != getTimeStamp(archive)) 139 || (prefs.getLong(KEY_CRC, NO_VALUE) != currentCrc); 140 } 141 142 private static long getTimeStamp(File archive) { 143 long timeStamp = archive.lastModified(); 144 if (timeStamp == NO_VALUE) { 145 // never return NO_VALUE 146 timeStamp--; 147 } 148 return timeStamp; 149 } 150 151 152 private static long getZipCrc(File archive) throws IOException { 153 long computedValue = ZipUtil.getZipCrc(archive); 154 if (computedValue == NO_VALUE) { 155 // never return NO_VALUE 156 computedValue--; 157 } 158 return computedValue; 159 } 160 161 private static List<File> performExtractions(File sourceApk, File dexDir) 162 throws IOException { 163 164 final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT; 165 166 // Ensure that whatever deletions happen in prepareDexDir only happen if the zip that 167 // contains a secondary dex file in there is not consistent with the latest apk. Otherwise, 168 // multi-process race conditions can cause a crash loop where one process deletes the zip 169 // while another had created it. 170 prepareDexDir(dexDir, extractedFilePrefix); 171 172 List<File> files = new ArrayList<File>(); 173 174 final ZipFile apk = new ZipFile(sourceApk); 175 try { 176 177 int secondaryNumber = 2; 178 179 ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX); 180 while (dexFile != null) { 181 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX; 182 File extractedFile = new File(dexDir, fileName); 183 files.add(extractedFile); 184 185 Log.i(TAG, "Extraction is needed for file " + extractedFile); 186 int numAttempts = 0; 187 boolean isExtractionSuccessful = false; 188 while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) { 189 numAttempts++; 190 191 // Create a zip file (extractedFile) containing only the secondary dex file 192 // (dexFile) from the apk. 193 extract(apk, dexFile, extractedFile, extractedFilePrefix); 194 195 // Verify that the extracted file is indeed a zip file. 196 isExtractionSuccessful = verifyZipFile(extractedFile); 197 198 // Log the sha1 of the extracted zip file 199 Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") + 200 " - length " + extractedFile.getAbsolutePath() + ": " + 201 extractedFile.length()); 202 if (!isExtractionSuccessful) { 203 // Delete the extracted file 204 extractedFile.delete(); 205 if (extractedFile.exists()) { 206 Log.w(TAG, "Failed to delete corrupted secondary dex ‘" + 207 extractedFile.getPath() + "‘"); 208 } 209 } 210 } 211 if (!isExtractionSuccessful) { 212 throw new IOException("Could not create zip file " + 213 extractedFile.getAbsolutePath() + " for secondary dex (" + 214 secondaryNumber + ")"); 215 } 216 secondaryNumber++; 217 dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX); 218 } 219 } finally { 220 try { 221 apk.close(); 222 } catch (IOException e) { 223 Log.w(TAG, "Failed to close resource", e); 224 } 225 } 226 227 return files; 228 } 229 230 private static void putStoredApkInfo(Context context, long timeStamp, long crc, 231 int totalDexNumber) { 232 SharedPreferences prefs = getMultiDexPreferences(context); 233 SharedPreferences.Editor edit = prefs.edit(); 234 edit.putLong(KEY_TIME_STAMP, timeStamp); 235 edit.putLong(KEY_CRC, crc); 236 /* SharedPreferences.Editor doc says that apply() and commit() "atomically performs the 237 * requested modifications" it should be OK to rely on saving the dex files number (getting 238 * old number value would go along with old crc and time stamp). 239 */ 240 edit.putInt(KEY_DEX_NUMBER, totalDexNumber); 241 apply(edit); 242 } 243 244 private static SharedPreferences getMultiDexPreferences(Context context) { 245 return context.getSharedPreferences(PREFS_FILE, 246 Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB 247 ? Context.MODE_PRIVATE 248 : Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS); 249 } 250 251 /** 252 * This removes any files that do not have the correct prefix. 253 */ 254 private static void prepareDexDir(File dexDir, final String extractedFilePrefix) 255 throws IOException { 256 dexDir.mkdirs(); 257 if (!dexDir.isDirectory()) { 258 throw new IOException("Failed to create dex directory " + dexDir.getPath()); 259 } 260 261 // Clean possible old files 262 FileFilter filter = new FileFilter() { 263 264 @Override 265 public boolean accept(File pathname) { 266 return !pathname.getName().startsWith(extractedFilePrefix); 267 } 268 }; 269 File[] files = dexDir.listFiles(filter); 270 if (files == null) { 271 Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ")."); 272 return; 273 } 274 for (File oldFile : files) { 275 Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " + 276 oldFile.length()); 277 if (!oldFile.delete()) { 278 Log.w(TAG, "Failed to delete old file " + oldFile.getPath()); 279 } else { 280 Log.i(TAG, "Deleted old file " + oldFile.getPath()); 281 } 282 } 283 } 284 285 private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, 286 String extractedFilePrefix) throws IOException, FileNotFoundException { 287 288 InputStream in = apk.getInputStream(dexFile); 289 ZipOutputStream out = null; 290 File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX, 291 extractTo.getParentFile()); 292 Log.i(TAG, "Extracting " + tmp.getPath()); 293 try { 294 out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp))); 295 try { 296 ZipEntry classesDex = new ZipEntry("classes.dex"); 297 // keep zip entry time since it is the criteria used by Dalvik 298 classesDex.setTime(dexFile.getTime()); 299 out.putNextEntry(classesDex); 300 301 byte[] buffer = new byte[BUFFER_SIZE]; 302 int length = in.read(buffer); 303 while (length != -1) { 304 out.write(buffer, 0, length); 305 length = in.read(buffer); 306 } 307 out.closeEntry(); 308 } finally { 309 out.close(); 310 } 311 Log.i(TAG, "Renaming to " + extractTo.getPath()); 312 if (!tmp.renameTo(extractTo)) { 313 throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + 314 "\" to \"" + extractTo.getAbsolutePath() + "\""); 315 } 316 } finally { 317 closeQuietly(in); 318 tmp.delete(); // return status ignored 319 } 320 } 321 322 /** 323 * Returns whether the file is a valid zip file. 324 */ 325 static boolean verifyZipFile(File file) { 326 try { 327 ZipFile zipFile = new ZipFile(file); 328 try { 329 zipFile.close(); 330 return true; 331 } catch (IOException e) { 332 Log.w(TAG, "Failed to close zip file: " + file.getAbsolutePath()); 333 } 334 } catch (ZipException ex) { 335 Log.w(TAG, "File " + file.getAbsolutePath() + " is not a valid zip file.", ex); 336 } catch (IOException ex) { 337 Log.w(TAG, "Got an IOException trying to open zip file: " + file.getAbsolutePath(), ex); 338 } 339 return false; 340 } 341 342 /** 343 * Closes the given {@code Closeable}. Suppresses any IO exceptions. 344 */ 345 private static void closeQuietly(Closeable closeable) { 346 try { 347 closeable.close(); 348 } catch (IOException e) { 349 Log.w(TAG, "Failed to close resource", e); 350 } 351 } 352 353 // The following is taken from SharedPreferencesCompat to avoid having a dependency of the 354 // multidex support library on another support library. 355 private static Method sApplyMethod; // final 356 static { 357 try { 358 Class<?> cls = SharedPreferences.Editor.class; 359 sApplyMethod = cls.getMethod("apply"); 360 } catch (NoSuchMethodException unused) { 361 sApplyMethod = null; 362 } 363 } 364 365 private static void apply(SharedPreferences.Editor editor) { 366 if (sApplyMethod != null) { 367 try { 368 sApplyMethod.invoke(editor); 369 return; 370 } catch (InvocationTargetException unused) { 371 // fall through 372 } catch (IllegalAccessException unused) { 373 // fall through 374 } 375 } 376 editor.commit(); 377 } 378 }
ZipUtil类
1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 /* Apache Harmony HEADER because the code in this class comes mostly from ZipFile, ZipEntry and 18 * ZipConstants from android libcore. 19 */ 20 21 package android.support.multidex; 22 23 import java.io.File; 24 import java.io.IOException; 25 import java.io.RandomAccessFile; 26 import java.util.zip.CRC32; 27 import java.util.zip.ZipException; 28 29 /** 30 * Tools to build a quick partial crc of zip files. 31 */ 32 final class ZipUtil { 33 static class CentralDirectory { 34 long offset; 35 long size; 36 } 37 38 /* redefine those constant here because of bug 13721174 preventing to compile using the 39 * constants defined in ZipFile */ 40 private static final int ENDHDR = 22; 41 private static final int ENDSIG = 0x6054b50; 42 43 /** 44 * Size of reading buffers. 45 */ 46 private static final int BUFFER_SIZE = 0x4000; 47 48 /** 49 * Compute crc32 of the central directory of an apk. The central directory contains 50 * the crc32 of each entries in the zip so the computed result is considered valid for the whole 51 * zip file. Does not support zip64 nor multidisk but it should be OK for now since ZipFile does 52 * not either. 53 */ 54 static long getZipCrc(File apk) throws IOException { 55 RandomAccessFile raf = new RandomAccessFile(apk, "r"); 56 try { 57 CentralDirectory dir = findCentralDirectory(raf); 58 59 return computeCrcOfCentralDir(raf, dir); 60 } finally { 61 raf.close(); 62 } 63 } 64 65 /* Package visible for testing */ 66 static CentralDirectory findCentralDirectory(RandomAccessFile raf) throws IOException, 67 ZipException { 68 long scanOffset = raf.length() - ENDHDR; 69 if (scanOffset < 0) { 70 throw new ZipException("File too short to be a zip file: " + raf.length()); 71 } 72 73 long stopOffset = scanOffset - 0x10000 /* ".ZIP file comment"‘s max length */; 74 if (stopOffset < 0) { 75 stopOffset = 0; 76 } 77 78 int endSig = Integer.reverseBytes(ENDSIG); 79 while (true) { 80 raf.seek(scanOffset); 81 if (raf.readInt() == endSig) { 82 break; 83 } 84 85 scanOffset--; 86 if (scanOffset < stopOffset) { 87 throw new ZipException("End Of Central Directory signature not found"); 88 } 89 } 90 // Read the End Of Central Directory. ENDHDR includes the signature 91 // bytes, 92 // which we‘ve already read. 93 94 // Pull out the information we need. 95 raf.skipBytes(2); // diskNumber 96 raf.skipBytes(2); // diskWithCentralDir 97 raf.skipBytes(2); // numEntries 98 raf.skipBytes(2); // totalNumEntries 99 CentralDirectory dir = new CentralDirectory(); 100 dir.size = Integer.reverseBytes(raf.readInt()) & 0xFFFFFFFFL; 101 dir.offset = Integer.reverseBytes(raf.readInt()) & 0xFFFFFFFFL; 102 return dir; 103 } 104 105 /* Package visible for testing */ 106 static long computeCrcOfCentralDir(RandomAccessFile raf, CentralDirectory dir) 107 throws IOException { 108 CRC32 crc = new CRC32(); 109 long stillToRead = dir.size; 110 raf.seek(dir.offset); 111 int length = (int) Math.min(BUFFER_SIZE, stillToRead); 112 byte[] buffer = new byte[BUFFER_SIZE]; 113 length = raf.read(buffer, 0, length); 114 while (length != -1) { 115 crc.update(buffer, 0, length); 116 stillToRead -= length; 117 if (stillToRead == 0) { 118 break; 119 } 120 length = (int) Math.min(BUFFER_SIZE, stillToRead); 121 length = raf.read(buffer, 0, length); 122 } 123 return crc.getValue(); 124 } 125 }