實(shí)戰(zhàn)庫ImplLoader的介紹
首先來介紹一下實(shí)戰(zhàn)項(xiàng)目的所解決的問題 : 當(dāng)一個(gè)Android工程中如果已經(jīng)使用不同的module來做業(yè)務(wù)隔離。那我們就可能有這種需求,module1想實(shí)例化一個(gè)module2的類,一般要怎么解決呢?
-
module1依賴module2 - 把
module2的這個(gè)類沉到底層庫,然后module1和module2都使用這個(gè)底層庫。 - ....等
下面來介紹一個(gè)小庫 : ImplLoader。可以很方便解決這個(gè)問題。只需這樣使用即可:
- 使用
@Impl標(biāo)記需要被加載的類
//`module2`中的類:
@Impl(name = "module2_text_view")
public class CommonView extends AppCompatTextView {
}
- 使用
ImplLoader.getImpl("module2_text_view")來獲取這個(gè)類
public class Module1Page extends LinearLayout {
public Module1Page(@NonNull Context context) {
super(context);
init();
}
private void init() {
//根據(jù)name,獲取需要加載的類
View module1Tv = ImplLoader.getView(getContext(), "module2_text_view");
addView(module1Tv);
}
}
- 初始化
ImplLoader
ImplLoader.init()
庫的代碼放在: https://github.com/SusionSuc/ImplLoader
為什么要寫這個(gè)庫 ?
主要是為了練手
在閱讀WMRouter和ARouter源碼時(shí)發(fā)現(xiàn)這兩個(gè)庫都用到了自定義注解、自定義gradle插件、Gradle Transfrom API、javapoet和asm庫。而我對于這些知識(shí)很多我只是了解個(gè)大概,或者壓根就沒聽說過。
因此ImplLoader這個(gè)庫主要是用來熟悉這個(gè)知識(shí)的。當(dāng)然這個(gè)庫的實(shí)現(xiàn)思路主要參考WMRouter和ARouter。
庫的實(shí)現(xiàn)原理
用下面這種圖概括一下:

其實(shí)整個(gè)庫代碼并不多,不過實(shí)現(xiàn)起來用到的東西不少,如果一些你使用的不熟悉,可以先看一下:
這個(gè)庫是用來總結(jié)我這兩年Android所學(xué)和對自我提高的一個(gè)庫。里面的文章我寫的很用心,會(huì)一直頻繁更新。
下面簡單過一下ImplLoader的實(shí)現(xiàn)代碼(只看主流程):
定義@Impl注解
@Retention(RetentionPolicy.RUNTIME)
public @interface Impl {
String name() default "";
}
編譯時(shí)注解處理器ImplAnnotationProcessor, 掃描@Impl,并生成ImplInfo_XXX.java
//ImplAnnotationProcessor.process()
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
.....
HashMap<String, ImplAnnotationInfo> implMap = new HashMap<>(); //用來保存掃描到的注解信息
for (Element implElement : roundEnv.getElementsAnnotatedWith(Impl.class)) {
ImplAnnotationInfo implAnnotationInfo = getImplAnnotationInfo((TypeElement) implElement);
implMap.put(implAnnotationInfo.name, implAnnotationInfo);
}
//生成 ImplInfo_xxx.java
new ImplClassProtocolGenerate(elementsUitls, filer).generateImplProtocolClass(implMap);
return true;
}
//生成 ImplInfo_xxx.java
void generateImplProtocolClass(HashMap<String, ImplAnnotationInfo> implMap) {
TypeSpec.Builder implInfoSpec = getImplInfoSpec();
MethodSpec.Builder implInfoMethodSpec = getImplInfoMethodSpec();
for (String implName : implMap.keySet()) {
CodeBlock registerBlock = getImplInfoInitCode(implMap.get(implName));
implInfoMethodSpec.addCode(registerBlock);
}
implProtocolSpec.addMethod(implInfoMethodSpec.build());
writeImplProtocolCode(implInfoSpec.build());
}
Gradle Transfrom掃描生成的ImplInfo_XXX.java文件,并生成ImplLoaderHelp.class
//ImplLoaderTransform.java
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
Set<String> implInfoClasses = new HashSet<>();
for (TransformInput input : transformInvocation.getInputs()) {
input.getJarInputs().forEach(jarInput -> {
try {
File jarFile = jarInput.getFile();
File dst = transformInvocation.getOutputProvider().getContentLocation(
jarInput.getName(), jarInput.getContentTypes(), jarInput.getScopes(),
Format.JAR);
implInfoClasses.addAll(InsertImplInfoCode.getImplInfoClassesFromJar(jarFile));
FileUtils.copyFile(jarFile, dst); //必須要把輸入,copy到輸出,不然接下來沒有辦法處理
} catch (IOException e) {
}
});
input.getDirectoryInputs().forEach(directoryInput -> {
//......
});
}
File dest = transformInvocation.getOutputProvider().getContentLocation(
"ImplLoader", TransformManager.CONTENT_CLASS,
ImmutableSet.of(QualifiedContent.Scope.PROJECT), Format.DIRECTORY);
InsertImplInfoCode.insertImplInfoInitMethod(implInfoClasses, dest.getAbsolutePath());
}
// 新產(chǎn)生一個(gè)類
public static void insertImplInfoInitMethod(Set<String> implInfoClasses, String outputDirPath) {
.....
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, writer) {};
String className = ProtocolConstants.IMPL_LOADER_HELP_CLASS.replace('.', '/');
cv.visit(50, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", null);
MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC,ProtocolConstants.IMPL_LOADER_HELP_INIT_METHOD, "()V", null, null);
mv.visitCode();
for (String clazz : implInfoClasses) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC, clazz.replace('.', '/'),
ProtocolConstants.IMPL_INFO_CLASS_INIT_METHOD,
"()V",
false);
}
mv.visitMaxs(0, 0);
mv.visitInsn(Opcodes.RETURN);
mv.visitEnd();
cv.visitEnd();
File dest = new File(outputDirPath, className + SdkConstants.DOT_CLASS);
dest.getParentFile().mkdirs();
new FileOutputStream(dest).write(writer.toByteArray());
}
運(yùn)行時(shí)反射實(shí)例化ImplLoaderHelp.class,并調(diào)用init方法,來加載@Impl注冊的類
object ImplLoader {
//保存 @Impl注冊的類
private val implMap = HashMap<String, Class<*>>()
@JvmStatic
fun init() {
try {
Class.forName(ProtocolConstants.IMPL_LOADER_HELP_CLASS)
.getMethod(ProtocolConstants.IMPL_LOADER_HELP_INIT_METHOD)
.invoke(null)
} catch (e: Exception) {}
}
//在生成的 ImplInfo_XX.java文件中會(huì)調(diào)用
fun registerImpl(implName: String, implClass: Class<*>) {
implMap.put(implName, implClass)
}
... 獲取實(shí)例相關(guān)方法....
}
實(shí)現(xiàn)過程中遇到的一些問題
注解處理器庫的創(chuàng)建
整個(gè)項(xiàng)目我是建了一個(gè)AndroidProject。因?yàn)樽⒔鈳熘粫?huì)在編譯的時(shí)候用到,因此我單獨(dú)建了一個(gè)Android Library庫,用來存放注解處理相關(guān)代碼??墒窃趯懙臅r(shí)候,發(fā)現(xiàn)找不到javax.annotation下注解相關(guān)類。后來發(fā)現(xiàn)原因是新建的Android Library是不會(huì)包含這寫庫的,需要新建一個(gè)Java Library
如何調(diào)試注解處理器 和 Gradle Transfrom
注解處理器代碼編寫完了?怎么調(diào)試呢? 具體參考 : https://blog.csdn.net/jeasonlzy/article/details/74273851 這篇文章,我把如何調(diào)試注解處理器這段搬過來:
- 在項(xiàng)目根目錄下的gradle.properties中添加如下兩行配置
org.gradle.daemon=true //記得把創(chuàng)建項(xiàng)目自動(dòng)創(chuàng)建寫的那個(gè)注釋掉
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5006
- 打開運(yùn)行配置,添加一個(gè)遠(yuǎn)程調(diào)試如下, 其中name可以任意取,port端口號(hào)就是上面一步指定的端口號(hào)。

- 切換運(yùn)行配置到切換剛剛創(chuàng)建的processor,然后點(diǎn)擊debug按鈕

- 最后,在我們需要調(diào)試的地方打上斷點(diǎn),然后再次點(diǎn)擊編譯按鈕(小錘子按鈕),即可進(jìn)入斷點(diǎn)
上面這4步也適用于調(diào)試Gradle Transform
上傳自定義的 Gradle Transform插件到本地目錄然后引用
編寫完成Gradle Transform Plugin之后我怎么使用了?上傳到maven然后依賴? 不太現(xiàn)實(shí),因?yàn)槲乙恢闭{(diào)試。最后決定這樣解決:
- 把插件上傳到工程下的一個(gè)目錄(作為maven倉庫)
apply plugin : 'maven'
group 'com.susion.loaderplugin'
version '0.0.1'
uploadArchives {
repositories {
flatDir {
name "../localRepo"
dir "../localRepo/libs"
}
}
}
- 在主工程的
build.gradle引入本地maven庫
buildscript {
repositories {
flatDir {
name 'localRepo'
dir "localRepo/libs/implloader"
}
}
dependencies {
classpath 'com.susion.loaderplugin:loaderplugin:0.0.1'
}
}
- 在demo引入插件
apply plugin: 'com.susion.loaderplugin'
經(jīng)過這樣操作后,整個(gè)插件開發(fā)將會(huì)非常方便。
支持kotlin
對于java文件,如果要處理其中的注解,我們可以這樣引入我們的注解處理器:
annotationProcessor project(":compiler")
但是當(dāng)我在module中創(chuàng)建了一個(gè)kotlin文件,并標(biāo)記@Impl后我發(fā)現(xiàn)。我自定義的注解處理器并不能掃描到kotlin文件上的注解。如果想要讓注解處理器在kotlin文件上生效需要對帶有kotlin代碼的工程,加上kotlin的注解處理插件:
apply plugin: 'kotlin-kapt' //引入 kotlin kapt
dependencies {
.....
kapt project(':compiler')
}
庫的上傳
決定將庫上傳到maven,但因?yàn)?code>ImplLoader的實(shí)現(xiàn)涉及到4個(gè)庫 loaderplugin、loadercore、annotation-interface和compiler。因此想要使用一個(gè)統(tǒng)一的腳本來上傳這4個(gè)庫到binary
首先在主項(xiàng)目的build.gradle中引入binary插件依賴
buildscript {
repositories {
jcenter()
mavenCentral()
}
dependencies {
.....
classpath 'com.github.dcendents:android-maven-gradle-plugin:latest.release'
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.6'
}
}
使用下面這個(gè)腳本統(tǒng)一做上傳:
apply plugin: 'com.github.dcendents.android-maven'
apply plugin: 'com.jfrog.bintray'
group = "com.susion.implloader"
version = "1.0.0"
//一些敏感的信息放在 local.properties 中
def getPropertyFromLocalProperties(key) {
File file = project.rootProject.file('local.properties')
if (file.exists()) {
Properties properties = new Properties()
properties.load(file.newDataInputStream())
return properties.getProperty(key)
}
}
bintray {
user = getPropertyFromLocalProperties("bintray.user")
key = getPropertyFromLocalProperties("bintray.apikey")
configurations = ['archives']
pkg {
repo = 'maven'
name = "${project.group}:${project.name}"
userOrg = "${project.name}"
licenses = ['Apache-2.0']
websiteUrl = 'https://github.com/SusionSuc'
vcsUrl = ''
publish = true
}
}
即每個(gè)庫的 artifactedId為:project.name。
最后在對于的module中使用這個(gè)腳本即可。
還有一些小問題這里先不講述了。歡迎關(guān)注我的 : https://github.com/SusionSuc/AdvancedAndroid