内容简介:app在启动的时候:从手机外部内存加载dex文件和网络获取dex文件其实最后都是一个意思,这里我们就用外部内存来举例。图解如下:
app在启动的时候:
从手机外部内存加载dex文件和网络获取dex文件其实最后都是一个意思,这里我们就用外部内存来举例。
- 1,先去加载手机外部内存的dex文件,也就是我们修复好的dex文件,再通过反射加载到我们自己的dexElements数组。
- 2,然后再通过反射去拿到系统的dex文件数组。
- 3,创建一个新的Array数组,将自己的dex文件数组依次加载到新创建的Array里面,再去将系统的dex数组也加载到新创建的Array数组里面。
- 4,最后用新创建的Array数组,通过反射去替换掉系统的dexElements数组。
图解如下:
图画的比较丑
接下来就是代码部分
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(); } } } } 复制代码
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。