Mac下 Multidex热更新学习笔记

本文介绍了如何在Mac环境下,通过Multidex实现Android应用的热更新。首先在Gradle配置中启用MultiDex,然后创建自定义Application子类。接着,通过dx工具将修改后的class文件编译为dex文件,并将其放置到手机指定目录完成热更新。这种方法避免了重新打包APK,提高了修复效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第一步,在项目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>


其它的Java文件
Test.java,很简单,通过人为的制造一个数学计算错误
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();
    }
}
FixDexUtils.java 这里主要使用了反射的知识
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;
    }
}
第五步
通过以上代码,可以得知:
1.当程序运行起来后,直接点击Run按钮,程序是直接崩溃的
2.那Fix按钮点击之后,就能处理热修复了吗?答案是否定的,这里还需要处理关于已修复的class文件打包成dex的问题。

另外,还有一个要点,就是把AndroidStudio默认的热更新编译关闭!关闭!关闭!去掉勾选状态

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文件。如下图

根据热修复的理解,我们需要把dex文件替换。所以把这个.class字节码文件打包成classes2.dex
我们把这个已经修复bug的Test.java编译好的Test.class复制出去。
比如我这里是保存在 桌面的MyDex文件夹中。 注意Test.class文件外面的文件夹一定要和其包名一致

就是说我创建了一个MyDex文件夹,而Test.class文件一定是这样的路径放进去的 MyDex/jp/sinya/multidexdemo1/test/Test.class

5.这个时候就需要用到build-tools工具。找到sdk目录,选择一个版本,最好是当前这个项目使用的版本。我这里使用的是26.0.2版本


打开命令行工具,cd进入到这个 26.0.2目录下(当然也可以配置环境变量,然后直接调用,这里不再作介绍)
假设现在已经进入到了 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应用的目录中。


7.这个时候重新启动app程序(不是run编译跑程序)。
第一次同样的点击 run 还是崩溃。
再次启动,然后这次先点击 fix,再点击run。发现程序不会再崩溃,直接Toast 弹出 10. 


好了,到这里基本的简单实现热更新的原理。谢谢大家

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值