Android热修复简易实现

https://www.jianshu.com/p/b65e5da3dff2 

  • 先了解一下原理和实现方式

  • 个人博客:https://yunhao.fun

Java编译为class

javac xxx.java.

class打包为jar包

jar cvf xxx.jar x/x/x/class #可用.来代替目录 意思为当前目录和所有的子目录打包

将as编译好的jar包拆开

unzip a.jar -d outfile
jar -xvf a.jar

jar包转为dex

./d8 --output xxx-file xxx.jar

代码

工具类

package com.example.hotfix;

import android.content.Context;
import android.util.Log;
import android.widget.Toast;

import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;

import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;

public class MyFix {

    public static void fixDexFile(Context context, String fixDexFilePath) {
        if (fixDexFilePath != null && new File(fixDexFilePath).exists()) {
            try {
                injectDexToClassLoader(context, fixDexFilePath);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    private static void injectDexToClassLoader(Context context, String fixDexFilePath)
            throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException{
        // 旧数组
        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
        Object basePathList = getPathList(pathClassLoader);
        Object baseElements = getDexElements(basePathList);
//        System.out.println("原数组为:" + Arrays.toString((Object[]) baseElements));
        // 修复数组
        String baseDexAbsolutePath = context.getDir("dex", 0).getAbsolutePath();
        DexClassLoader fixDexClassLoader = new DexClassLoader(
                fixDexFilePath, baseDexAbsolutePath, fixDexFilePath, context.getClassLoader());
        Object fixPathList = getPathList(fixDexClassLoader);
        Object fixElements = getDexElements(fixPathList);
        // 新数组
        Object newElements = combineArray(baseElements, fixElements);
        System.out.println("新数组为:" + Arrays.toString((Object[]) newElements));
        Object basePathList2 = getPathList(pathClassLoader);
        //新数组替换旧数组
        setField(basePathList2, basePathList2.getClass(), "dexElements", newElements);
    }
    public static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,
            IllegalAccessException {
        return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }
    public static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
        return getField(obj, obj.getClass(), "dexElements");
    }
    private static Object getField(Object obj, Class cls, String str)
            throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = cls.getDeclaredField(str);
        declaredField.setAccessible(true);//设置为可访问
        return declaredField.get(obj);
    }
    private static void setField(Object obj, Class cls, String str, Object obj2)
            throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = cls.getDeclaredField(str);
        declaredField.setAccessible(true);//设置为可访问
        declaredField.set(obj, obj2);
    }
    private static Object combineArray(Object baseElements, Object fixElements) {
        Class componentType = fixElements.getClass().getComponentType();
        int length = Array.getLength(fixElements);
        int length2 = Array.getLength(baseElements) + length;
        Object newInstance = Array.newInstance(componentType, length2);
        for (int i = 0; i < length2; i++) {
            if (i < length) {
                //修复数在新数组的“0”
                Array.set(newInstance, i, Array.get(fixElements, i));
            } else {
                Array.set(newInstance, i, Array.get(baseElements, i - length));
            }
        }
        return newInstance;
    }
}

MainActivity

package com.example.hotfix;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import java.io.File;
import java.lang.reflect.Array;
import java.util.Arrays;

import dalvik.system.BaseDexClassLoader;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;

public class MainActivity extends AppCompatActivity {
    private static final String DEX_DIR = "patch";
    private static String TAG = "Mainactivity";
    private Context context;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button btn_run = findViewById(R.id.btn_run);
        Button btn_repair = findViewById(R.id.btn_repair);
         context = MainActivity.this;
        checkFix();

        init();
        btn_run.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                new BugTest().getbug();
            }
        });
        btn_repair.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                try {
                    checkFix();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    private void init() {

        File patchDir = new File(this.getExternalFilesDir(null), DEX_DIR);
        if (!patchDir.exists()) {
            patchDir.mkdirs();
        }
    }
//    private void checkStoragePermission() {
//        if (ContextCompat.checkSelfPermission(this,
//                Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
//                || ContextCompat.checkSelfPermission(this,
//                Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
//            // 如果权限没有被授予,则请求权限
//            ActivityCompat.requestPermissions(this,
//                    new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
//                            android.Manifest.permission.WRITE_EXTERNAL_STORAGE},
//                    REQUEST_CODE_STORAGE_PERMISSION);
//            Log.e(TAG, "开始请求权限");
//        } else {
//            Log.e(TAG, "权限已经拿到");
//            // 如果权限已经被授予,则可以执行相应的操作
//            // 例如读取或写入文件
//        }
//    }
//
//    @Override
//    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
//        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
//        if (requestCode == REQUEST_CODE_STORAGE_PERMISSION) {
//            if (grantResults.length > 0
//                    && grantResults[0] == PackageManager.PERMISSION_GRANTED
//                    && grantResults[1] == PackageManager.PERMISSION_GRANTED) {
//
//            } else {
//                // 如果用户拒绝了权限请求,则可以显示一个提示信息,或者采取其他措施
//                if (shouldShowRequestPermissionRationale(Manifest.permission.READ_EXTERNAL_STORAGE)
//                        || shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
//                    // 再次请求权限
//                    ActivityCompat.requestPermissions(this,
//                            new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
//                                    Manifest.permission.WRITE_EXTERNAL_STORAGE},
//                            REQUEST_CODE_STORAGE_PERMISSION);
//                } else {
//                    // 如果用户拒绝了权限请求,并且选择了“不再询问”选项,可以显示一个提示信息
//                    Toast.makeText(this, "权限被拒绝,某些功能可能无法正常工作", Toast.LENGTH_SHORT).show();
//                }
//            }
//        }
//    }
//
//    public Context getContext() {
//        Context context = MainActivity.this;
//        return context;
//    }
    private void checkFix() {
        try {
//            String dexPath = Environment.getExternalStorageDirectory() + "/classes.dex";
//            String dexPath = String.valueOf(context.getExternalFilesDir(DEX_DIR+"/classes.dex"));
            String dexPath = "/storage/emulated/0/Android/data/com.example.hotfix/files/patch/classes.dex";
//            String dexPath="/data/data/com.example.hotfix/cache/classes.dex";
            Log.e(TAG, "dex文件路径为"+dexPath);
            new MyFix().fixDexFile(this, dexPath);
        } catch (Exception e) {
            Toast.makeText(this, "修复失败" + e.getMessage(), Toast.LENGTH_SHORT).show();
            e.printStackTrace();
        }
    }
    private void getarrly() {
        try {
            PathClassLoader pathClassLoader = (PathClassLoader) MainActivity.this.getClassLoader();
            Object basePathList = new MyFix().getPathList(pathClassLoader);
            Object baseElements = new MyFix().getDexElements(basePathList);
            System.out.println("原数组为:" + Arrays.toString((Object[]) baseElements));
        }catch (Exception e){
            e.printStackTrace();
        }


    }
    @Override
    protected void onStart() {
        super.onStart();
        getarrly();

    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
    }

    @Override
    protected void onRestart() {
        super.onRestart();
    }

    @Override
    protected void onResume() {
        super.onResume();
    }

    @Override
    protected void onStop() {
        super.onStop();
    }

}

补充

  • 修改后的dex数组因为双亲模型的原因 需要尽快的进行加载 重写attachBaseContext 将加载放置于之中

  • 双亲模型特性同一个classloader,同一个class 只会加载一次

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值