若是你對本文感興趣,也許你對個人公衆號也會有興趣,可掃下方二維碼或搜索公衆微信號:mxszgg html
若是讀者對模塊化開發的服務調用具備必定的認識能夠跳過下面一小節。java
模塊化開發如今對於 Android 開發者來講應該是一個耳熟能詳的名詞了,如今應該有許多應用的開發迭代都使用了模塊化開發,模塊化開發的意義是在於將 App 的業務細分紅 N 個模塊,利於開發人員的協做開發。模塊化開發當中有一個須要解決的問題就是模塊之間的服務調用——由於各個模塊是以 library 形式存在,彼此之間不相互依賴,故使彼此之間實際上並不知道對方的存在,那麼當 A 模塊想要知道 B 模塊中的某個信息,須要調用 B 中的某個方法時該怎麼辦呢?例如開發人員當前正在 main 模塊開發,當前的一個 TextView 須要展現電影信息,可是很明顯電影信息這塊屬於 movie 模塊而並非 main 模塊,那麼此時該如何解決呢?機智的 Android 開發人員建立了基礎模塊 service 並讓全部的業務模塊依賴 service 模塊,service 模塊的職責也很簡單,只須要提供接口聲明,具體的實現就交給具體的業務模塊本身去實現了。例如 service 模塊中提供一個 MovieService 類:android
public interface MovieService {
String movieName();
}
複製代碼
那麼在 movie 模塊中就能夠建立一個 MovieServiceImpl 類去實現 MovieService 接口了——git
public class MovieServiceImpl implements MovieService {
@Override public String movieName() {
return "一出好戲";
}
}
複製代碼
而對於 main 模塊來講,它應該調用 MovieService 實現類的 movieName()
方法就行了,可是事實上 main 模塊又不可能知道 MovieService 的具體實現類是什麼,因此看起來彷佛問題又卡住了...github
實際上問題在於如何獲取到接口實現類的路徑,例如 renxuelong/ComponentDemo 中所提到的,反射調用全部模塊的 application 的某個方法,在該方法中將接口與實現類映射起來,該方法的弊端很明顯,開發者須要顯示填寫全部模塊 application 的徹底限定名,這在開發中應當是儘可能避免的。api
流行的解決方案就是 ARouter 的實現方式了——使用 APT—— build 時掃描全部的被 @Route
註解所修飾的類,判斷該類是否實現了某個接口,若是是的話則建立相應的 xxx$$app
類,讀者能夠下載 ARouter 的 demo 在 build 以後找到 ARouter$$Providers$$app
類 ——bash
如上圖所示,左側是接口的徹底限定名,右側是具體的實現類,這樣就將接口與實現類一一映射起來了,相比於上面所提到的方法,開發者並不須要手動地去填寫類的徹底限定名,由於在實際開發中類的路徑是極可能被改變的,這種撰寫類的徹底限定名的操做應該避免由開發者去作,而應該去交給構建工具去完成。微信
實際上筆者本文所想要闡述的方案與 APT 的原理是同樣的,經過掃描指定註解所修飾的類獲取到全部的 service 接口的實現類,並用 Map 將其維護起來。app
結合官方文檔文檔上來講,Transform 是一個類,構建工具中自帶諸如 ProGuardTransform
、DexTransform
等 Transform,一系列的 Transform 類將全部的 .class 文件轉換爲 .dex 文件,而官方容許開發者建立自定義的 Transform 來操做轉換成 .dex 文件以前的全部 .class 文件,這意味着開發者能夠對app 中全部的 .class 文件進行操做。開發者能夠在插件中經過 android.registerTransform(theTransform)
或者 android.registerTransform(theTransform, dependencies)
來註冊一個 Transform。maven
前面提到,Transform 其實是一系列的操做,因此開發者應該很容易理解,前一個 Transform 的輸出理應會是下一個 Transform 的輸入——
關於理解本文所須要的 Transform 知識先說到這,其餘涉及的知識點會在後文的實操中提到。若是各位讀者對 Transform 想要深一步瞭解,更多 Transform 使用姿式可參考官方文檔。
javassist 是一個字節碼工具,簡單來講能夠利用它來增刪改 .class 文件的代碼,畢竟在構建時期的 .java 文件都編譯成了 .class 文件了。
在動手寫代碼前應該思考一下須要建立幾個 lib 工程,對於模塊化開發中的各個 module 來講,它們總共須要兩個類,一個是註解,若是當前 module 有接口服務須要實現,那麼得用這個註解來標記實現類;另外一個就是 Map,須要經過它來獲取其餘 module 的實現類。固然,除了建立前面所提到的這個 lib 工程之外,還須要建立一個 plugin 供 app 模塊使用。
新建一個 java 模塊取名爲 hunter,並建立 HunterRegistry 類和 Impl 註解以下:
public final class HunterRegistry {
private static Map<Class<?>, Object> services;
private HunterRegistry() {
}
@SuppressWarnings("unchecked") public static <T> T get(Class<T> key) {
return (T) services.get(key);
}
}
複製代碼
public @interface Impl {
Class<?> service();
}
複製代碼
對於 main 模塊來講,若是它想要獲取 movie 模塊的電影信息,它僅需調用 HunterRegistry.get(MovieService.class).movieName()
便可得到 MovieService 實現類的具體方法實現,HunterRegistry 類看起來有些匪夷所思,services
對象甚至都沒有初始化,因此調用 get()
方法必定會報錯,從現有代碼看起來確實是這樣可是實際上在 Transform 中獲取到全部的接口-實現類的映射關係以後將會經過 javassist 插入靜態代碼初始化 services
對象並向 services
對象中 put 鍵值對,最終生成 .class 文件相似以下:
public final class HunterRegistry {
private static Map<Class<?>, Object> services = new HashMap();
static {
services.put(MovieService.class, new MovieServiceImpl());
}
private HunterRegistry() {
}
@SuppressWarnings("unchecked") public static <T> T get(Class<T> key) {
return (T) services.get(key);
}
}
複製代碼
而對於 movie 模塊來講,它須要建立 MovieService 的具體實現類,並用 @Impl
註解標記以便 Transform 能夠找到它與接口的映射關係,例如:
@Impl(service = MovieService.class)
public class MovieServiceImpl implements MovieService {
@Override public String movieName() {
return "一出好戲";
}
}
複製代碼
接下來就是建立 gradle plugin 了:
建立 plugin 的基本過程本文就不說起了,若是讀者不太清楚的話,能夠參考筆者以前寫的寫給 Android 開發者的 Gradle 系列(三)撰寫 plugin。
建立一個 plugin 類,plugin 的內容很簡單:
class HunterPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.plugins.withId('com.android.application') {
project.android.registerTransform(new HunterTransform())
}
}
}
複製代碼
因此能夠看得出來全部的重點就是在這個 HunterTransform 身上了——
class HunterTransform extends Transform {
private static final String CLASS_REGISTRY = 'com.joker.hunter.HunterRegistry'
private static final String CLASS_REGISTRY_PATH = 'com/joker/hunter/HunterRegistry.class'
private static final String ANNOTATION_IMPL = 'com.joker.hunter.Impl'
private static final Logger LOG = Logging.getLogger(HunterTransform.class)
@Override
String getName() {
return "hunterService"
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return Collections.singleton(QualifiedContent.Scope.SUB_PROJECTS)
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(TransformInvocation transformInvocation)
throws TransformException, InterruptedException, IOException {
// 1
transformInvocation.outputProvider.deleteAll()
def pool = ClassPool.getDefault()
JarInput registryJarInput
def impls = []
// 2
transformInvocation.inputs.each { input ->
input.jarInputs.each { JarInput jarInput ->
pool.appendClassPath(jarInput.file.absolutePath)
if (new JarFile(jarInput.file).getEntry(CLASS_REGISTRY_PATH) != null) {
registryJarInput = jarInput
LOG.info("registryJarInput.file.path is ${registryJarInput.file.absolutePath}")
} else {
def jarFile = new JarFile(jarInput.file)
jarFile.entries().grep { entry -> entry.name.endsWith(".class") }.each { entry ->
InputStream stream = jarFile.getInputStream(entry)
if (stream != null) {
CtClass ctClass = pool.makeClass(stream)
if (ctClass.hasAnnotation(ANNOTATION_IMPL)) {
impls.add(ctClass)
}
ctClass.detach()
}
}
FileUtils.copyFile(jarInput.file,
transformInvocation.outputProvider.getContentLocation(jarInput.name,
jarInput.contentTypes, jarInput.scopes, Format.JAR))
LOG.info("jarInput.file.path is $jarInput.file.absolutePath")
}
}
}
if (registryJarInput == null) {
return
}
// 3
def stringBuilder = new StringBuilder()
stringBuilder.append('{\n')
stringBuilder.append('services = new java.util.HashMap();')
impls.each { CtClass ctClass ->
ClassFile classFile = ctClass.getClassFile()
AnnotationsAttribute attr = (AnnotationsAttribute) classFile.getAttribute(
AnnotationsAttribute.invisibleTag)
Annotation annotation = attr.getAnnotation(ANNOTATION_IMPL)
def value = annotation.getMemberValue('service')
stringBuilder.append('services.put(')
.append(value)
.append(', new ')
.append(ctClass.name)
.append('());\n')
}
stringBuilder.append('}\n')
LOG.info(stringBuilder.toString())
def registryClz = pool.get(CLASS_REGISTRY)
registryClz.makeClassInitializer().setBody(stringBuilder.toString())
// 4
def outDir = transformInvocation.outputProvider.getContentLocation(registryJarInput.name,
registryJarInput.contentTypes, registryJarInput.scopes, Format.JAR)
copyJar(registryJarInput.file, outDir, CLASS_REGISTRY_PATH, registryClz.toBytecode())
}
private void copyJar(File srcFile, File outDir, String fileName, byte[] bytes) {
outDir.getParentFile().mkdirs()
def jarOutputStream = new JarOutputStream(new FileOutputStream(outDir))
def buffer = new byte[1024]
int read = 0
def jarFile = new JarFile(srcFile)
jarFile.entries().each { JarEntry jarEntry ->
if (jarEntry.name == fileName) {
jarOutputStream.putNextEntry(new JarEntry(fileName))
jarOutputStream.write(bytes)
} else {
jarOutputStream.putNextEntry(jarEntry)
def inputStream = jarFile.getInputStream(jarEntry)
while ((read = inputStream.read(buffer)) != -1) {
jarOutputStream.write(buffer, 0, read)
}
}
}
jarOutputStream.close()
}
}
複製代碼
這裏簡單提一下前三個方法,首先是 getInputTypes()
,它表示輸入該 Transform 的文件類型是什麼,從 QualifiedContent.ContentType 的實現類中能夠看到仍是有不少種輸入文件類型的,然並卵,前文提到,官方只容許開發者對 .class 文件操做,固然,這裏咱們也只須要對 .class 文件操做就行了,因此這裏得填 TransformManager.CONTENT_CLASS
;接着是 getScopes()
方法,它表示開發者須要從哪些地方獲取這些輸入文件,而 QualifiedContent.Scope.SUB_PROJECTS
就是表明各個 module,由於咱們也只須要獲取各個 module 的 .class 文件就行了;最後是 isIncremental()
方法,它表明當前 Transform 是否支持增量編譯,爲了使得本文所談到的內容更簡單一些,筆者選擇了 return false
表明當前 Transform 不支持增量編譯,各位讀者後期能夠參考官方文檔優化這個 Transform 使其支持增量編譯。接下來就是核心的 transform()
方法了——爲了方便解釋代碼,筆者將 transform()
方法分紅了4個部分,首先第1部分爲了不上一次構建對本次構建的影響,須要調用 transformInvocation.outputProvider.deleteAll()
刪除上一次構建的產物,以及一些初始化的操做;第2部分就是對 Transform 輸入產物的操做了,也就是全部的 .class 文件,input 除了 jarInputs 以外還有 dirInputs,可是對於輸入範圍爲 QualifiedContent.Scope.SUB_PROJECTS
的 Transform 來講輸入類型只有 jarInputs,而這裏的 jarInputs.file 其實是當前項目中全部 module:
在這一步中,咱們要區分出兩類 jar,一類是包含 HunterRegistry.class 的 jar 包,經過 new JarFile(jarInput.file).getEntry(CLASS_REGISTRY_PATH) != null
便可判斷當前 jar 包是否包含 HunterRegistry.class 也就是上面截圖的 hunter.jar;而另外一類就是 module 的 jar 包,經過 groovy 的 api 篩選出 jar 包中全部的 .class 文件,再依靠 javassist 提供的 api 判斷當前 .class 是不是被 @Impl
註解所修飾的,若是是的話就將它添加到 impls 裏面,前文提到前一個 Transform 的輸出會是下一個 Transform 的輸入,因此須要經過 transformInvocation.outputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
獲取該 jar 包應該移動到的路徑下,由於它還要做爲下一個 Transform 的輸入;第3步就是利用 impls 獲取具體實現類,利用 javassist api 獲取 @Impl
註解中的 service 方法的返回值,也就是接口類,再將它們拼接成字符串,最終再經過 registryClz.makeClassInitializer().setBody(stringBuilder.toString())
便可將這段字符串注入到 HunterRegistry.class 文件中了;第4步就是將上一步獲取到的新 HunterRegistry.class 文件的字節碼替換掉原先的字節碼並最後打入指定的路徑下就行了。
經過 jadx 工具打開 debug.apk 再找到 HunterRegistry.class 文件,字節碼以下:
能夠看到 MovieService 和它的實現類 MovieServiceImpl 被 put 進了 services 當中。運行 debug.apk 跳轉到 main 模塊下 HomeActivity 就能夠看到屏幕上的輸出值了:
不管是 APT 方案仍是 Transform 方案,它們所解決模塊化開發中的服務調用核心思想都是在於找到接口與實現類的映射關係,只要解決了映射關係,問題也就迎刃而解了。若是是暫不瞭解 Transform 的讀者,筆者認爲在瞭解完本文的知識後,能夠更深一步的去了解 Transform,例如優化 HunterTransform
,使其支持增量編譯;例如嘗試改變輸入範圍後,輸入的文件會有什麼不同?
當輸入範圍爲
而當輸入範圍爲QualifiedContent.Scope.PROJECT
時輸入的文件中將會有 directoryInput 類型,其文件夾路徑實際上就是../app/build/intermediates/classes/debug
,實際上裏面就是 app 模塊的全部 .class 文件:QualifiedContent.Scope.EXTERNAL_LIBRARIES
時輸入的 jar 包所有都是第三方庫:因此若是將插件傳到 maven,以第三方形式以來進工程的話,那麼輸入範圍就不能僅僅是上文提到的
QualifiedContent.Scope.SUB_PROJECTS
了,由於插件的 jar 包將會找不到。
最後是項目地址:jokermonn/transformSample