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

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

内容简介: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();
        }
    }
}
}
复制代码

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

查看所有标签

猜你喜欢:

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

Numerical Linear Algebra

Numerical Linear Algebra

Lloyd N. Trefethen、David Bau III / SIAM: Society for Industrial and Applied Mathematics / 1997-06-01 / USD 61.00

Numerical Linear Algebra is a concise, insightful, and elegant introduction to the field of numerical linear algebra.一起来看看 《Numerical Linear Algebra》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具