基础巩固---DexClassLoader是如何加载插件中的类?
在上次https://www.cnblogs.com/webor2006/p/12267707.html插件化的学习中已经实现了Activity的跳转了,在继续开启新的学习之前,先来对这块代码的原理进行一下剖析:
![](https://i-blog.csdnimg.cn/blog_migrate/92ee6e2407da1a3e6bcde36ee4a20a36.png)
其实关于Android中是如何加载类的在当时https://www.cnblogs.com/webor2006/p/10708754.html手写热修复tinker中已经详细阐述过了,但是时隔这么久了有些细节也忘了,另外还有一些新的细节可以挖掘的,所以这里再重新对其进行梳理一下,温故知新:
先来看一下Android类加载器的类结构:
![](https://i-blog.csdnimg.cn/blog_migrate/5d7476fe5d97c1ddc76719ae98a32645.png)
![](https://i-blog.csdnimg.cn/blog_migrate/9641086b9039cfab6a00d1d2d00e769d.png)
其中对于我们平常接触最多的就是标红的两个,而对于这次插件化而言咱们是用的DexClassLoader来进行插件类的加载的,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/5163e234cd3371f0d548ea6615474e63.png)
好,接下来从这个构造分析起,点进去发现看不到源码:
![](https://i-blog.csdnimg.cn/blog_migrate/44b0c48b3e4fd4ddea5ad67eec5575a2.png)
由于我们目前是用的9.0的API,而目前没有下9.0关于dalvik的源码,目前只有8.0的,而我本地vpn此时又挂了,改用在线http://androidxref.com/9.0.0_r3/的方式吧体验又比较差,所以有一个笨办法,将相关要读的源码上这个在线网站上查找,然后再手动将其下载下来并放到本地sdk的源码中既可,比如:
![](https://i-blog.csdnimg.cn/blog_migrate/68e774b49c371c2c9af757440c1a0dad.png)
然后手动再将相关的源码下到本地,不过这种下载比较麻烦,得一个个进去才行,比如:
![](https://i-blog.csdnimg.cn/blog_migrate/20ca2050c736203036914afe4a35609e.png)
不过有个小技巧就是先记住这个下载地址,比如这个类的下载地址为:http://androidxref.com/9.0.0_r3/raw/libcore/dalvik/src/main/java/dalvik/annotation/AnnotationDefault.java
那很显然我们要下载其它类的话,直接只要改路径既可,这样就比一个个要进到java源码中再下还是要稍稍快些,具体就不多说了,纯体力活,当然如果有vpn的话这一些烦锁的操作就省啦,当然可以多学一种看源码的方式有益无害~~
下载完之后,我们再手动将下下来的源码放到SDK中的源码目录中既可,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/5dfe0330acfd67c034bb9df7129e4185.png)
![](https://i-blog.csdnimg.cn/blog_migrate/919bd0a8f7a6663bed108a826cdde118.png)
由于目录工程的tartgetSdk用的是10.0,所以将API改为28:
![](https://i-blog.csdnimg.cn/blog_migrate/1f3e5cb4b78c4e31de2d91d96caaa3b9.png)
好,此时就可以在Android Studio正常的读源码啦,先来看DexClassLoader的构造:
![](https://i-blog.csdnimg.cn/blog_migrate/198a3605bf35c3a3f693a1f796425738.png)
调用父类的构造了:
![](https://i-blog.csdnimg.cn/blog_migrate/0acf2d525add7cbd44d20465742341f7.png)
好,此时再看一下DexPathList构造做了啥?
![](https://i-blog.csdnimg.cn/blog_migrate/488af77be9bee2d09c2e55435dfb9f4b.png)
其中dexElements是不是相当的熟,对于像tinker这样热修复的实现主要是靠这个东东:
![](https://i-blog.csdnimg.cn/blog_migrate/98e07ac535bb39e328cca4f8f396e9c5.png)
好,来看一下makeDexElements做了啥?
![](https://i-blog.csdnimg.cn/blog_migrate/fc4326c8ac83ab515e633b986ceb5116.png)
其实就是遍历apk中的dex文件,每个dex都会封装成一个Element对象,然后此时再看一下loadDexFile()的细节:
![](https://i-blog.csdnimg.cn/blog_migrate/51426bc451787eb0020da7624468c815.png)
此时又创建了一个DexFile对象,看一下它构建的细节:
![](https://i-blog.csdnimg.cn/blog_migrate/ed57a57233d9c1633570ce6f8a38418a.png)
然后此时打开DexFile文件了:
![](https://i-blog.csdnimg.cn/blog_migrate/b3dc53cc12d18625836cfc8155235dc2.png)
![](https://i-blog.csdnimg.cn/blog_migrate/0bc55ccd56cc82aaeb6143688a85c242.png)
最终是由NDK底层来打开Dex文件的,当loadDexFile()之后,则会将其放到Element的集合中:
![](https://i-blog.csdnimg.cn/blog_migrate/d49277bfb6f30ee152f5763f982c6b5d.png)
用图总结一下这句话的背后流程:
![](https://i-blog.csdnimg.cn/blog_migrate/a4b37e3cbe3c3ea0065f4d195df1800a.png)
![](https://i-blog.csdnimg.cn/blog_migrate/5d500a7f9c6ad29c12f70dfa048e9fcd.png)
接下来我们在代理类中会用这个ClassLoader进行类的查找,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/7ac8b3991fe6fc7cfd1ec2ba2f159070.png)
其中getClassLoader()是重写过了的,返回的就是我们自己创建的DexClassLoader,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/5c43c7fbd7228f8f0edb5e25a77106e5.png)
接下来则来看一下整个查找类的过程,这样整个原理就清楚了:
![](https://i-blog.csdnimg.cn/blog_migrate/6b3df19d50b05cab79cc35587e0eb1dc.png)
此时它的流程在当时手写tinker博文中已经详细阐述过了,双亲委派机制就展现得淋漓尽致,最终会调用它子类的,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/c4b70604172ddcbee339893a28ff77ed.png)
![](https://i-blog.csdnimg.cn/blog_migrate/6ce0452fd3aa1d79feab5c85636525cb.png)
此时跳到子类来看一下查找的细节:
![](https://i-blog.csdnimg.cn/blog_migrate/fe093b0ef617a3f3e5df632080a7f1dc.png)
看到它是不是就非常之熟了,它里面肯定就是遍历在初始化Dex的Elements数组中进行类的查找嘛,跟进去再确定一下:
![](https://i-blog.csdnimg.cn/blog_migrate/19e5aff18ee0f8983da8873430b0b4d3.png)
最终查看类还是通过ndk底层来查的,最后跟进去搂一眼:
![](https://i-blog.csdnimg.cn/blog_migrate/48763447a3c7f5633fdd08bc362c7e44.png)
![](https://i-blog.csdnimg.cn/blog_migrate/42c3972f990a76287eb653e4c735ff06.png)
![](https://i-blog.csdnimg.cn/blog_migrate/6009dd4d8dbf0060155b9982fd0568d3.png)
至此~~关于整个插件中类的查找流程就分析完了,有之前热修复的基础的话其实还是不难的。
插庄式实现Service的调用:
在之前已经实现在插件中Activity的调用了,四大组件中还有一个比较重要的是Service,那它又是如何来调用呢?由于木有上下文,所以肯定还得依赖于插桩的Service,其整体实现思路跟Activity其实差不多,下面来实现一下:
先来在公共模块处定义一个抽象接口:
![](https://i-blog.csdnimg.cn/blog_migrate/925394b8ea03fecb82dcd821eabcf249.png)
package com.android.pluginstand;
import android.app.Service;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.IBinder;
public interface InterfaceService {
void onCreate();
void onStart(Intent intent, int startId);
int onStartCommand(Intent intent, int flags, int startId);
void onDestroy();
void onConfigurationChanged(Configuration newConfig);
void onLowMemory();
void onTrimMemory(int level);
IBinder onBind(Intent intent);
boolean onUnbind(Intent intent);
void onRebind(Intent intent);
void onTaskRemoved(Intent rootIntent);
void attach(Service proxyService);
}
在插件那块也得封装一个BaseService,来将所有里面的Service进行统一,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/6568f60130766e9f808b27b1651f3b31.png)
然后里面新建一个具体的子类:
![](https://i-blog.csdnimg.cn/blog_migrate/b7d81e1bb0d84ff93415e5a7d648a830.png)
注意:此时也不需要在清单文件中对这个服务进行注册的,好,接下来再回到宿主搞一个插桩服务,其实现逻辑基本上代理的Activity类似,直接贴出代码了:
![](https://i-blog.csdnimg.cn/blog_migrate/3759e21b3e20d683b85df82f162203f9.png)
package com.android.pluginarchstudy;
import android.app.Service;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import com.android.pluginstand.InterfaceService;
import java.lang.reflect.Constructor;
public class ProxyService extends Service {
private String serviceName;
private InterfaceService interfaceService;
@Override
public IBinder onBind(Intent intent) {
init(intent);
return null;
}
private void init(Intent intent) {
serviceName = intent.getStringExtra("serviceName");
//加载service 类
try {
//插件oneService
Class<?> aClass = getClassLoader().loadClass(serviceName);
Constructor constructor = aClass.getConstructor(new Class[]{});
Object in = constructor.newInstance(new Object[]{});
interfaceService = (InterfaceService) in;
interfaceService.attach(this);
Bundle bundle = new Bundle();
bundle.putInt("from", 1);
interfaceService.onCreate();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public ClassLoader getClassLoader() {
return PluginManager.getInstance().getDexClassLoader();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (interfaceService == null) {
init(intent);
}
return interfaceService.onStartCommand(intent, flags, startId);
}
@Override
public boolean onUnbind(Intent intent) {
interfaceService.onUnbind(intent);
return super.onUnbind(intent);
}
}
然后在清单中注册一下:
![](https://i-blog.csdnimg.cn/blog_migrate/3a6df3f3b3c7ed67fadba0cb7f6b8896.png)
好,接下来应用一下,咱们在插件跳转到第二个界面中来开启咱们的服务:
![](https://i-blog.csdnimg.cn/blog_migrate/42c2c35758000e30191e5a8944c589d1.png)
很显然此时的startService需要重写一下,因为上下文的问题:
![](https://i-blog.csdnimg.cn/blog_migrate/dd098f7a247edb369d92f5b4a0fee64e.png)
然后此时还需要重写个东东:
![](https://i-blog.csdnimg.cn/blog_migrate/b8478a89c99cbccdf2f3ee40629a1328.png)
最终得在代理Activity中重写一下startService才行,因为得转到咱们的ProxyService才行的,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/16bcfb7b99b6332a0920a8192ff810cb.png)
ok,一切就续,验证一下看是否正常,先来build一个插件更新到sdcard中,然后再运行宿主app,运行如下:
![](https://i-blog.csdnimg.cn/blog_migrate/bd9ac2195c907397df94415f31503f1e.gif)
完美实现~~
插庄式实现动态广播的调用:
对于Android的广播注册都知道分为静态和动态注册,这里先来实现一下动态广播,因为静态广播的要复杂一些,待下一次再来仔细学习,这里先把动态广播的调用给实现了,其实现思路基本雷同,先来定义公共方法:
![](https://i-blog.csdnimg.cn/blog_migrate/cf33508179a6613d38eb92edcf9fd5ad.png)
然后再在插件中定义一个基类的广播:
![](https://i-blog.csdnimg.cn/blog_migrate/cd4388c5ecaafaab37de38f716dabaf7.png)
此时也不需要往清单中进行广播的注册,但是如果是静态广播肯定还是得注册的,不过这个之后再说,目前焦点是来调用动态广播既可,接下来则来注册一下这个广播,还是在插件中点击跳转到第二个界面时进行注册,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/5fa42c1473a5bdebe479231b96ca9373.png)
此时同样得重写这个registerReceiver,不多解释了:
![](https://i-blog.csdnimg.cn/blog_migrate/ce11d20c00ee4f89c4eb20f13f96df08.png)
好,接下来则需要回到宿主来写一个插桩的Receiver,逻辑类似,直接贴出代码,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/1c57f01972370ea798a1f412fa59412e.png)
然后在清单中注册一下:
![](https://i-blog.csdnimg.cn/blog_migrate/4e7e5140b198cab02c5478807d0d2c1f.png)
最后还得到我们的代理Activity中来重写一下registerReceiver方法,因为我们在插件中这样调用了:
![](https://i-blog.csdnimg.cn/blog_migrate/e683c3c02d359c4288ae0361d052eae6.png)
所以:
![](https://i-blog.csdnimg.cn/blog_migrate/c0153abdd3327bb5c617260e776410ef.png)
最后咱们来发送应用一下,咱们还是到插件的主界面中增加一个按钮进行发送测试:
![](https://i-blog.csdnimg.cn/blog_migrate/2979bf1e31212966cf36a6ceba4a7b7d.png)
![](https://i-blog.csdnimg.cn/blog_migrate/4c6203985e0f5792895f7ec62bc1b519.png)
此时又因为上下文的问题得重写一个sendBroadcast才行:
![](https://i-blog.csdnimg.cn/blog_migrate/be0488b44f6e69762280b79ed3582b0a.png)
好,代码一切就绪,接下来重新更新一个插件,并运行宿主看一下:
![](https://i-blog.csdnimg.cn/blog_migrate/dbbe91b6a579833b834df7a30ef66e38.gif)
妥妥的,不过这里还有个待优化的代码,就是注册广播了还没有对它做注销处理,比较简单下面来写一下:
![](https://i-blog.csdnimg.cn/blog_migrate/e28a7494503e3413a536b98e1cf79f47.png)
![](https://i-blog.csdnimg.cn/blog_migrate/d6c2f60a96cd3647b9c10d41b06f67ae.png)
再重写一下宿主的unregisterReceiver方法:
![](https://i-blog.csdnimg.cn/blog_migrate/aff2309645ee4202845a9ce4bd51f882.png)
ok,还有另一种静态广播的调用下一次再学~~