Tinker热修复手写简单实现原理

栏目: Android · 发布时间: 5年前

内容简介:app在启动的时候:从手机外部内存加载dex文件和网络获取dex文件其实最后都是一个意思,这里我们就用外部内存来举例。图解如下:

app在启动的时候:

从手机外部内存加载dex文件和网络获取dex文件其实最后都是一个意思,这里我们就用外部内存来举例。

  • 1,先去加载手机外部内存的dex文件,也就是我们修复好的dex文件,再通过反射加载到我们自己的dexElements数组。
  • 2,然后再通过反射去拿到系统的dex文件数组。
  • 3,创建一个新的Array数组,将自己的dex文件数组依次加载到新创建的Array里面,再去将系统的dex数组也加载到新创建的Array数组里面。
  • 4,最后用新创建的Array数组,通过反射去替换掉系统的dexElements数组。

图解如下:

图画的比较丑

Tinker热修复手写简单实现原理

接下来就是代码部分

1,首先我们新建一个工程。新创建一个FixDexUtils类,代码有注释,如下:

这个类里面主要是加载外部内存的dex文件,反射获取系统的dexElements数组,将自己的dexElements数组合系统的dexElements数组合并,最后是合并后的新数组反射替换系统的dexElements数组。

public class FixDexUtils {
//创建一个HashSet数组,用来装外部内存的dex文件
private static HashSet<File> loaderDex=new HashSet<>();
static {
    loaderDex.clear();
}
public static void loadDex(Context context){
    //修复  不止一次  按时间顺序  从外置内存卡的文件夹中拿dex文件
    File fileDir=context.getDir("odex",Context.MODE_PRIVATE);
    File[] listFiles=fileDir.listFiles();
    String optimizeDir=fileDir.getAbsolutePath()+File.separator+"opt_dex";
    File fopt=new File(optimizeDir);
    if (!fopt.exists()) {
        fopt.mkdirs();
    }
    for (File file : listFiles) {
        if (file.getName().startsWith("classes")||file.getName().endsWith(".dex")) {
            Log.d("FixDexUtils", "遍历文件:" + file.getAbsolutePath());
            loaderDex.add(file);
            DexClassLoader classLoader=new DexClassLoader(file.getAbsolutePath(),
                    optimizeDir,null,context.getClassLoader());
            //这个事真正用来加载class
            PathClassLoader pathClassLoader= (PathClassLoader) context.getClassLoader();

            try {
                //系统的ClassLoader  Elment[]
                Class baseDexClassLoader=Class.forName("dalvik.system.BaseDexClassLoader");
                Field pathListField=baseDexClassLoader.getDeclaredField("pathList");
                pathListField.setAccessible(true);
                Object pathListObject=pathListField.get(pathClassLoader);

                //获取 系统Element[]数组
                Class systemPathClazz=pathClassLoader.getClass();
                Field systemdexElementsField=systemPathClazz.getDeclaredField("dexElements");
                systemdexElementsField.setAccessible(true);
                //拿到系统 Element[]数组
                Object systemdexElements=systemdexElementsField.get(pathListObject);


                ////////////////////////////////////////////////////////////////////////////////////////
                //自己的classLoader  Elment[]
                Class myDexClassLoader=Class.forName("dalvik.system.BaseDexClassLoader");
                Field myPathListField=myDexClassLoader.getDeclaredField("pathList");
                myPathListField.setAccessible(true);
                Object myPathListObject=myPathListField.get(classLoader);


                Class myPathClazz=myPathListObject.getClass();
                Field myElementsField=myPathClazz.getDeclaredField("dexElements");
                myElementsField.setAccessible(true);
                Object myElements=myElementsField.get(myPathListObject);


                ////////////////////////////////////////////////////////////////////////////////////
                //融合
                //有一个新数组Element类型
                Class<?> sigleElementClazz=systemdexElements.getClass().getComponentType();
                //数组
                int systemLength=Array.getLength(systemdexElements);
                int myLength=Array.getLength(myElements);
                int allLength=systemLength+myLength;
                Object newElementArray= Array.newInstance(sigleElementClazz,allLength);
                for(int i=0;i<allLength;i++){
                    if(i<myLength){ //将自己的数组放在新创建的数组前面 ,是依次放
                        Array.set(newElementArray,i,Array.get(myElements,i));
                    }else {  //将系统的数组放在新创建数组的后面  //也是依次放
                        Array.set(newElementArray,i,Array.get(systemdexElements,i-myLength));
                    }
                }

                ///////////////////////////////这里是真正的融合的数组,放到系统的PathClassLaoder////////////////////////////////////////////////////
                //将新的数组装进系统
                Field elementsField=pathListField.getClass().getDeclaredField("dexElements");
                elementsField.setAccessible(true);
                elementsField.set(pathListObject,newElementArray);

            } catch (Exception e) {
                e.printStackTrace();
            }


        }
    }

    //

}
复制代码

}

2,由于有的时候,会分包,分包的目的在于解决65535问题,有第三方插件可以使用,如下:

将这个引入build.gradle文件中。

//这主要是分包用的
compile 'com.android.support:multidex:1.0.1'
复制代码

完整的代码如下:

引入了multidex后,还要在build.gradle里面作一些配置,代码里面有写,有注释,这里就不再详细写了。

android {
compileSdkVersion 26
buildToolsVersion "26.0.2"
defaultConfig {
    applicationId "com.gzshengye.tinkertext"
    minSdkVersion 15
    targetSdkVersion 26
    versionCode 1
    versionName "1.0"
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    //注意,也要在这里设置一下
    multiDexEnabled true
    flavorDimensions "versionCode"
}

//分包用的,用于支持多少,安卓版本兼容
productFlavors{
    dev{
        minSdkVersion 21
    }
    prod{
        minSdkVersion 14
    }
}

buildTypes {
    release {
        //如果单独设置这个,只会分包,但是不会分主包
        multiDexEnabled true
        //分主包用的
        multiDexKeepFile file('dex.keep')
        //获取主包
        def myFile=file('dex.keep')
        //打印主包是否存在
        println("是否存在:"+myFile.exists())
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}
dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:26.0.1'
    testCompile 'junit:junit:4.12'
    compile 'com.yanzhenjie:permission:1.0.5'
    //这主要是分包用的
    compile 'com.android.support:multidex:1.0.1'
}
复制代码

3,还需要在工程结构的app的目录下添加“dex.keep”文件,也就是刚刚在build.gradle里面配置的那个。我的文件里面代码如下:

配置dex.keep文件,主要的作用是,在文件里面配置的类,是分在主包的,也就是主dex文件里面,因为系统在加载dex文件时,主dex是不能出错的。它是有启动整个应用的作用。所以需要将一些关键的类放在dex.keep文件里面,不能分包进其他dex文件里。

com/gzshengye/tinkertext/MainActivity.class
com/gzshengye/tinkertext/MyApplication.class
复制代码

4,然后我们在MyApplication里面初始化MultiDex和FixDexUtils。完整代码如下:

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
    }
    @Override
    protected void attachBaseContext(Context base) {
        MultiDex.install(base);
        FixDexUtils.loadDex(base);
        super.attachBaseContext(base);
    }
}
复制代码

5,然后创建一个Compute类,这个类是用来测试bug用的。完整代码如下:

这里的10除以0,是肯定会报错的。那么我们放在外置内存里面的dex文件,肯定是10除以1,或者是除以其他不为0的数。

/**用于测试类
 * Created by Administrator on 2018/2/26.
 */
public class Compute {
    public  void compute(Context context){
        int i=10;
        int j=0;
        int k=i/j;
        Toast.makeText(context, "等于:"+k, Toast.LENGTH_SHORT).show();
    }
}
复制代码

6,在xml文件里面写上布局,完整的代码如下:

当app运行起来了,如果还没有修复,点击“计算”会报错,因为10除以0,肯定会报错的。然后我再点击“修复”,前提是修改好的dex文件要提前放在外置内存卡里面,这样才能找的到。修复成功后,点击计算,就正常了。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.gzshengye.tinkertext.MainActivity">

<Button
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="compute"
    android:text="计算"/>
<Button
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="repair"
    android:text="修复"/>
<TextView
    android:id="@+id/tv"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />
</LinearLayout>
复制代码

7,这里贴出MainActivity类的代码,由于我这里做了权限适配,它会用到读写权限。完整代码如下:

public class MainActivity extends AppCompatActivity {

private static final String TAG = "MainActivity";
private TextView tv;
private android.widget.LinearLayout activitymain;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    this.activitymain = (LinearLayout) findViewById(R.id.activity_main);
    this.tv = (TextView) findViewById(R.id.tv);

    //这是申请多个权限
    AndPermission.with(this)
            .requestCode(100)
            .permission(
                    Manifest.permission.WRITE_EXTERNAL_STORAGE,
                    Manifest.permission.READ_EXTERNAL_STORAGE)
            .send();

}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    //super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode==100) {
        AndPermission.onRequestPermissionsResult(requestCode,permissions,grantResults,listener);
    }else {
        Toast.makeText(this, "权限申请拒绝", Toast.LENGTH_SHORT).show();
    }
}

private PermissionListener listener=new PermissionListener() {
    @Override
    public void onSucceed(int requestCode, List<String> grantPermissions) {
        //权限申请成功回调
        if (requestCode==100) {
            Log.e("MainActivity", "权限申请成功");

        }else {
            Log.e("MainActivity", "权限申请出现101");
            Toast.makeText(MainActivity.this, "权限申请出现101", Toast.LENGTH_SHORT).show();
        }
    }

    //这是当用户点击不再询问的时候,同时又点击了拒绝,就会走这一步
    //注意,这里面要是不给用户的提示,就直接finish();要是用户点击了不再询问和拒绝,用户想再进入应用,就进不来了
    @Override
    public void onFailed(int requestCode, List<String> deniedPermissions) {
        //权限申请失败回调
        if(AndPermission.hasAlwaysDeniedPermission(MainActivity.this,deniedPermissions)){
            //这应该是跳转到权限设置页面的,但是暂时还没有找到1,该用什么代替
            Log.e("MainActivity", "权限申请失败");
            Toast.makeText(MainActivity.this, "权限申请失败", Toast.LENGTH_SHORT).show();
            AndPermission.defaultSettingDialog(MainActivity.this,1)
                    .setTitle("请开启权限")
                    .setMessage("没有权限,将无法使用,请手动去‘权限’里面将需要的权限全部打开")
                    .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            finish();
                        }
                    })
                    .setPositiveButton("去设置")
                    .show();
        }

    }
};


/**计算
 * @param view
 */
public void compute(View view){
    Compute compute=new Compute();
    compute.compute(this);
}

/**修复
 * @param view
 */
public void repair(View view){
    fixBug();
复制代码

// String name = "out.dex"; // String path = new File(Environment.getExternalStorageDirectory(), name).getAbsolutePath();

File filesDir = this.getDir("odex", Context.MODE_PRIVATE);
    String name = "out.dex";
    String filePath = new File(filesDir, name).getAbsolutePath();

    tv.setText(filePath);
}

private void fixBug() {
    File filesDir = this.getDir("odex", Context.MODE_PRIVATE);
    String name = "out.dex";
    String filePath = new File(filesDir, name).getAbsolutePath();
    Log.e(TAG, "路径::"+filePath);
    File file = new File(filePath);
    if (file.exists()) {
        file.delete();
    }
    InputStream is = null;
    FileOutputStream os = null;
    try {
        Log.i(TAG, "fixBug: " + new File(Environment.getExternalStorageDirectory(), name).getAbsolutePath());
        is = new FileInputStream(new File(Environment.getExternalStorageDirectory(), name));
        os = new FileOutputStream(filePath);
        int len = 0;
        byte[] buffer = new byte[1024];
        while ((len = is.read(buffer)) != -1) {
            os.write(buffer, 0, len);
        }
        File f = new File(filePath);
        if (f.exists()) {
            Toast.makeText(this, "dex overwrite", Toast.LENGTH_SHORT).show();
        }
        //FixManager.loadDex(this);
        FixDexUtils.loadDex(this);
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } finally {
        try {
            os.close();
            is.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}
}
复制代码

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Linux从入门到精通

Linux从入门到精通

刘忆智、等 / 清华大学出版社 / 2010-1-1 / 59.00元

linux是目前增长最迅速的操作系统。本书由浅入深、循序渐进地向读者介绍linux的基本使用和系统管理。全书内容包括linux概述、linux安装、linux基本配置、桌面环境基本操作、shell基本命令、文件和目录管理、软件包管理、磁盘管理、用户与用户组管理、进程管理、网络配置、浏览网页、收发邮件、文件传输和共享、远程登录、多媒体应用、图像浏览和处理、打印机配置、办公软件的使用、linux编程工......一起来看看 《Linux从入门到精通》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具