第一步,在项目app的gradle文件中添加如下操作
①在dependencies添加 'com.android.support:multidex;1.0.1' 的官方包
②defaultConfig中添加 multiDexEnabled true
③buildTypes中的release
第二步 继承Application,实现一个子类
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
}
@Override
protected void attachBaseContext(Context base) {
MultiDex.install(base);
super.attachBaseContext(base);
}
}
第三步 在 AndroidManifest.xml文件中 写入Application,以及申请权限
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="jp.sinya.multidexdemo1">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
第四步,接下来我们编写一个MainActivity,布局很简单,只有两个button,一个是run,一个是fix
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void run(View v) {
new Test().run(this);
}
public void fix(View view) {
fixBug();
}
private void fixBug() {
// 对应目录 /data/data/packageName/mydex/classes2.dex
File fileDir = getDir(FixDexUtils.DEX_DIR, Context.MODE_PRIVATE);
String filePath = fileDir.getAbsolutePath() + File.separator + name;
File file = new File(filePath);
if (file.exists()) {
file.delete();
}
InputStream inputStream = null;
FileOutputStream fileOutputStream = null;
try {
//下载已修复的dex,保存在 SD卡路径根目录下的 /01Sinya/classes2.dex
String downDexFilePath = Environment.getExternalStorageDirectory().getAbsoluteFile() + File.separator + "01Sinya/" + name;
inputStream = new FileInputStream(downDexFilePath);
fileOutputStream = new FileOutputStream(filePath);
int len = 0;
byte[] buf = new byte[1024];
while ((len = inputStream.read(buf)) != -1) {
fileOutputStream.write(buf, 0, len);
}
File newFile = new File(filePath);
if (newFile.exists()) {
Toast.makeText(this, "dex 迁移成功", Toast.LENGTH_SHORT).show();
}
//热修复
FixDexUtils.loadFixedDex(this);
} catch (Exception e) {
e.printStackTrace();
LogUtils.SinyaE(e.toString());
try {
if (inputStream != null) {
inputStream.close();
}
if (fileOutputStream != null) {
fileOutputStream.close();
}
} catch (Exception e2) {
LogUtils.SinyaE(e2.toString());
}
}
}
}
布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="jp.sinya.multidexdemo1.MainActivity">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Run"
android:onClick="run"
android:textAllCaps="false"
/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Fix"
android:onClick="fix"
android:textAllCaps="false"
/>
</LinearLayout>
public class Test {
public void run(Context context) {
int a = 10;
int b = 0;
Toast.makeText(context, "a/b=" + a / b, Toast.LENGTH_SHORT).show();
}
}
public class FixDexUtils {
/**
* app应用程序根目录下的mydex文件夹,下载好的dex修复文件 会被通过io流 拷贝到这里
*/
public static final String DEX_DIR = "mydex";
/**
* app应用程序路径下 自定义的文件夹。因为类加载器只能读取应用安装的路径下的文件
*/
public static final String LOCAL_DEX_DIR = "opt_dex";
private static HashSet<File> loadedDex = new HashSet<>();
static {
loadedDex.clear();
}
public static void loadFixedDex(Context context) {
if (context == null) {
return;
}
//遍历所有要修复的dex
File fileDir = context.getDir(DEX_DIR, Context.MODE_PRIVATE);
//拿到这个文件夹目录中的所有文件
File[] listFiles = fileDir.listFiles();
for (File file : listFiles) {
if (file.getName().startsWith("classes") && file.getName().endsWith(".dex")) {
loadedDex.add(file);//存入集合
}
}
LogUtils.Sinya("loadedDexList.size: " + loadedDex.size());
//新的已修复的dex,与之前手机系统中的dex进行合并
doDexInject(context, fileDir);
}
private static void doDexInject(Context context, File fileDir) {
String dirPath = fileDir.getAbsolutePath() + File.separator + LOCAL_DEX_DIR;
LogUtils.Sinya("dirPath: " + dirPath);
File copyFileDir = new File(dirPath);
if (!copyFileDir.exists()) {
copyFileDir.mkdirs();
}
try {
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
for (File dex : loadedDex) {
DexClassLoader classLoader = new DexClassLoader(//
dex.getAbsolutePath(),// dexPath
copyFileDir.getAbsolutePath(),// optimizedDirectory
null,// libraryPath
pathClassLoader);// ClassLoader parent
Object dexObj = getPathList(classLoader);
Object pathObj = getPathList(pathClassLoader);
Object dexElementList = getDexElements(dexObj);
Object pathDexElementList = getDexElements(pathObj);
Object dexElement = combineArray(dexElementList, pathDexElementList);
Object pathList = getPathList(pathClassLoader);
setField(pathList, pathList.getClass(), "dexElements", dexElement);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static Object getPathList(Object baseDexClassLoader) throws Exception {
return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
private static Object getDexElements(Object obj) throws Exception {
return getField(obj, obj.getClass(), "dexElements");
}
private static Object getField(Object obj, Class<?> clazz, String fieldName) throws Exception {
Field localField = clazz.getDeclaredField(fieldName);
localField.setAccessible(true);
return localField.get(obj);
}
private static void setField(Object obj, Class<?> clazz, String fieldName, Object value) throws Exception {
Field localFiled = clazz.getDeclaredField(fieldName);
localFiled.setAccessible(true);
localFiled.set(obj, value);
}
/**
* 合并两个数组
*
* @param arrayLhs
* @param arrayRhs
* @return
*/
private static Object combineArray(Object arrayLhs, Object arrayRhs) {
Class<?> localClass = arrayLhs.getClass().getComponentType();
int i = Array.getLength(arrayLhs);
int j = i + Array.getLength(arrayRhs);
Object result = Array.newInstance(localClass, j);
for (int k = 0; k < j; k++) {
if (k < i) {
Array.set(result, k, Array.get(arrayLhs, k));
} else {
Array.set(result, k, Array.get(arrayRhs, k - i));
}
}
return result;
}
}
3.那么我们把目光回到Test.java类。 我们把变量b改为1,使之Toast会弹出 10/1 的结果 是10,这样再编译跑之后的程序就没有问题了。打住?what?这样还叫热修复,明明是重新打包了apk安装了好吗?(╯‵□′)╯︵┻━┻
public class Test {
public void run(Context context) {
int a = 10;
int b = 1;//这里,改成1
Toast.makeText(context, "a/b=" + a / b, Toast.LENGTH_SHORT).show();
}
}
4.这个时候我们是可以不需要编译跑程序的,可以build代码,然后通过AndroidStudio,找到Test.java重新编译之后的 .class文件。如下图
就是说我创建了一个MyDex文件夹,而Test.class文件一定是这样的路径放进去的 MyDex/jp/sinya/multidexdemo1/test/Test.class
5.这个时候就需要用到build-tools工具。找到sdk目录,选择一个版本,最好是当前这个项目使用的版本。我这里使用的是26.0.2版本
bash dx --dex --output=/Users/koizumisinya/Desktop/Apk/MyDex/classes2.dex /Users/koizumisinya/Desktop/Apk/MyDex
上述语句解释
--output=表示输出的文件路径 这里我把文件名命名为 classes2.dex(一定要是dex文件),然后保存的路径就是 MyDex根路径下,方便一会查找结果。
后面的路径,就是对应的要编译的目录,这里是只写到了 MyDex目录,它会自动的识别这个目录下的所有class文件,所以自然的也需要class文件对应其包目录。
好了,执行完上面的命令行,就会在MyDex目录下得到一个 classes2.dex文件
6.然后把这个文件放到手机对应的目录下面,忘记了目录名字,就看一下上面的代码。(我这里是放在 sd卡根目录下的 01Sinya/mydex/目录中)
其实这个过程你们也可以尝试着做成 从服务器中下载的形式,下载好了直接保存到 app应用的目录中。
本文介绍了如何在Mac环境下,通过Multidex实现Android应用的热更新。首先在Gradle配置中启用MultiDex,然后创建自定义Application子类。接着,通过dx工具将修改后的class文件编译为dex文件,并将其放置到手机指定目录完成热更新。这种方法避免了重新打包APK,提高了修复效率。

被折叠的 条评论
为什么被折叠?



