ARouter源碼解析(五)

arouter-gradle-plugin version : 1.0.2

AutoRegister : https://github.com/luckybilly/AutoRegister

前言

====

在本系列的第一篇中講過(guò)凌蔬,ARouter 可以通過(guò)掃描 dex 文件中 class 的全類(lèi)名,來(lái)加載 compiler 生成的路由類(lèi)籍嘹。但這種方式影響性能燃异,并且效率也不高猫牡。所以在 ARouter v1.3.0 之后的版本中布疼,加入了自動(dòng)注冊(cè)的方式進(jìn)行路由表的加載居暖,自動(dòng)注冊(cè)可以縮短初始化時(shí)間,解決應(yīng)用加固導(dǎo)致無(wú)法直接訪(fǎng)問(wèn) dex 文件從而初始化失敗的問(wèn)題肛著。

那么自動(dòng)注冊(cè)到底是什么東東圆兵,為什么有這么強(qiáng)大的能力呢跺讯?

那么接下來(lái),我們就來(lái)分析分析衙傀。

預(yù)先需要了解的知識(shí)點(diǎn):

  • 自定義 gradle plugin

  • gradle transform api

  • 使用 asm 實(shí)現(xiàn)字節(jié)碼插樁

arouter-register

================

arouter-register 的入口就在 PluginLaunch


public class PluginLaunch implements Plugin<Project> {

 @Override

 public void apply(Project project) {

 def isApp = project.plugins.hasPlugin(AppPlugin)

  //only application module needs this plugin to generate register code

 if (isApp) {

 Logger.make(project)

 Logger.i('Project enable arouter-register plugin')

 def android = project.extensions.getByType(AppExtension)

 def transformImpl = new RegisterTransform(project)

  //init arouter-auto-register settings

 ArrayList<ScanSetting> list = new ArrayList<>(3)

 list.add(new ScanSetting('IRouteRoot'))

 list.add(new ScanSetting('IInterceptorGroup'))

 list.add(new ScanSetting('IProviderGroup'))

 RegisterTransform.registerList = list

  //register this plugin

 android.registerTransform(transformImpl)

 }

 }

}

從上面的代碼可知:

  • 只在 application module (一般都是 app module)生成自動(dòng)注冊(cè)的代碼抬吟;

  • 初始化了自動(dòng)注冊(cè)的設(shè)置萨咕,這樣自動(dòng)注冊(cè)就知道需要注冊(cè) IRouteRoot IInterceptorGroup IProviderGroup 這三者统抬;

  • 注冊(cè) RegisterTransform ,字節(jié)碼插樁將在 RegisterTransform 中完成危队;

可以看出聪建,重點(diǎn)就在 RegisterTransform 里面。那我們重點(diǎn)就關(guān)注下 RegisterTransform 的代碼茫陆,這里就貼出 transform 方法的源碼了金麸。(關(guān)于 Transform 的 InputTypes 和 Scopes 知識(shí)點(diǎn)在這就不講了,如有需要了解的同學(xué)可以看 Android 熱修復(fù)使用Gradle Plugin1.5改造Nuwa插件


class RegisterTransform extends Transform {

@Override

void transform(Context context, Collection<TransformInput> inputs

 , Collection<TransformInput> referencedInputs

 , TransformOutputProvider outputProvider

 , boolean isIncremental) throws IOException, TransformException, InterruptedException {

 Logger.i('Start scan register info in jar file.')

 long startTime = System.currentTimeMillis()

 boolean leftSlash = File.separator == '/'

 inputs.each { TransformInput input ->

  // 掃描所有的 jar 文件

 input.jarInputs.each { JarInput jarInput ->

 String destName = jarInput.name

  // rename jar files

 def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath)

 if (destName.endsWith(".jar")) {

 destName = destName.substring(0, destName.length() - 4)

 }

  // 輸入的 jar 文件

 File src = jarInput.file

  // 輸出的 jar 文件

 File dest = outputProvider.getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)

  // 掃描 jar 文件簿盅,查找實(shí)現(xiàn) IRouteRoot IInterceptorGroup IProviderGroup 接口的類(lèi)挥下,并且找到 LogisticsCenter 在哪個(gè) jar 文件中

  // 不掃描 com.android.support 開(kāi)頭的 jar

 if (ScanUtil.shouldProcessPreDexJar(src.absolutePath)) {

  // ScanUtil.scanJar 的代碼就不詳細(xì)展開(kāi)了,感興趣的同學(xué)可以自己去看下

 ScanUtil.scanJar(src, dest)

 }

 FileUtils.copyFile(src, dest)

 }

  // 掃描所有的 class 文件桨醋,查找實(shí)現(xiàn) IRouteRoot IInterceptorGroup IProviderGroup 接口的類(lèi)

  // 和掃描 jar 做差不多類(lèi)似的工作棚瘟。不同的點(diǎn)就是不用再去找 LogisticsCenter 類(lèi)

 input.directoryInputs.each { DirectoryInput directoryInput ->

 File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)

 String root = directoryInput.file.absolutePath

 if (!root.endsWith(File.separator))

 root += File.separator

 directoryInput.file.eachFileRecurse { File file ->

 def path = file.absolutePath.replace(root, '')

 if (!leftSlash) {

 path = path.replaceAll("\\\\", "/")

 }

  // 只處理 com/alibaba/android/arouter/routes/ 開(kāi)頭的 class

 if(file.isFile() && ScanUtil.shouldProcessClass(path)){

 ScanUtil.scanClass(file)

 }

 }

  // copy to dest

 FileUtils.copyDirectory(directoryInput.file, dest)

 }

 }

 Logger.i('Scan finish, current cost time ' + (System.currentTimeMillis() - startTime) + "ms")

  // 這里開(kāi)始字節(jié)碼插樁操作

 if (fileContainsInitClass) {

  // 遍歷之前找的 IRouteRoot IInterceptorGroup IProviderGroup

 registerList.each { ext ->

 Logger.i('Insert register code to file ' + fileContainsInitClass.absolutePath)

 if (ext.classList.isEmpty()) {

 Logger.e("No class implements found for interface:" + ext.interfaceName)

 } else {

 ext.classList.each {

 Logger.i(it)

 }

  // 對(duì) LogisticsCenter.class 做字節(jié)碼插樁

 RegisterCodeGenerator.insertInitCodeTo(ext)

 }

 }

 }

 Logger.i("Generate code finish, current cost time: " + (System.currentTimeMillis() - startTime) + "ms")

}

}

上面代碼的邏輯很清晰,按照之前設(shè)置好的 IRouteRoot IInterceptorGroup IProviderGroup 這三個(gè)接口喜最,然后掃描整個(gè)項(xiàng)目的代碼偎蘸,分別找到這三者各自的實(shí)現(xiàn)類(lèi),然后加入到集合中瞬内。最后在 LogisticsCenter 中實(shí)現(xiàn)字節(jié)碼插樁迷雪。

我們來(lái)詳細(xì)看下 RegisterCodeGenerator.insertInitCodeTo(ext) 的代碼


static void insertInitCodeTo(ScanSetting registerSetting) {

 if (registerSetting != null && !registerSetting.classList.isEmpty()) {

 RegisterCodeGenerator processor = new RegisterCodeGenerator(registerSetting)

  // RegisterTransform.fileContainsInitClass 就是包含了 LogisticsCenter.class 的那個(gè) jar 文件

 File file = RegisterTransform.fileContainsInitClass

 if (file.getName().endsWith('.jar'))

 // 開(kāi)始處理

 processor.insertInitCodeIntoJarFile(file)

 }

}

插入的操作在 insertInitCodeIntoJarFile 中實(shí)現(xiàn)。


private File insertInitCodeIntoJarFile(File jarFile) {

 if (jarFile) {

 def optJar = new File(jarFile.getParent(), jarFile.name + ".opt")

 if (optJar.exists())

 optJar.delete()

 def file = new JarFile(jarFile)

 Enumeration enumeration = file.entries()

 JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optJar))

  // 遍歷 jar 文件中的 class

 while (enumeration.hasMoreElements()) {

 JarEntry jarEntry = (JarEntry) enumeration.nextElement()

 String entryName = jarEntry.getName()

 ZipEntry zipEntry = new ZipEntry(entryName)

 InputStream inputStream = file.getInputStream(jarEntry)

 jarOutputStream.putNextEntry(zipEntry)

  // 如果是 LogisticsCenter.class 的話(huà)

 if (ScanSetting.GENERATE_TO_CLASS_FILE_NAME == entryName) {

 Logger.i('Insert init code to class >> ' + entryName)

 // 插樁操作

 def bytes = referHackWhenInit(inputStream)

 jarOutputStream.write(bytes)

 } else {

 jarOutputStream.write(IOUtils.toByteArray(inputStream))

 }

 inputStream.close()

 jarOutputStream.closeEntry()

 }

 jarOutputStream.close()

 file.close()

  // 把字節(jié)碼插樁的 jar 替換掉原來(lái)舊的 jar 文件

 if (jarFile.exists()) {

 jarFile.delete()

 }

 optJar.renameTo(jarFile)

 }

 return jarFile

}

字節(jié)碼插樁的代碼還在 referHackWhenInit 方法中虫蝶。


//refer hack class when object init

private byte[] referHackWhenInit(InputStream inputStream) {

 ClassReader cr = new ClassReader(inputStream)

 ClassWriter cw = new ClassWriter(cr, 0)

 ClassVisitor cv = new MyClassVisitor(Opcodes.ASM5, cw)

 cr.accept(cv, ClassReader.EXPAND_FRAMES)

 return cw.toByteArray()

}

class MyClassVisitor extends ClassVisitor {

 MyClassVisitor(int api, ClassVisitor cv) {

 super(api, cv)

 }

 void visit(int version, int access, String name, String signature,

 String superName, String[] interfaces) {

 super.visit(version, access, name, signature, superName, interfaces)

 }

 @Override

 MethodVisitor visitMethod(int access, String name, String desc,

 String signature, String[] exceptions) {

 MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions)

  // 對(duì) loadRouterMap 這個(gè)方法進(jìn)行代碼插入

 if (name == ScanSetting.GENERATE_TO_METHOD_NAME) {

 mv = new RouteMethodVisitor(Opcodes.ASM5, mv)

 }

 return mv

 }

}

class RouteMethodVisitor extends MethodVisitor {

 RouteMethodVisitor(int api, MethodVisitor mv) {

 super(api, mv)

 }

 @Override

 void visitInsn(int opcode) {

  // 插入的代碼在 return 之前

 if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {

 extension.classList.each { name ->

 name = name.replaceAll("/", ".")

 mv.visitLdcInsn(name)//這里的name就是之前掃描出來(lái)的 IRouteRoot IInterceptorGroup IProviderGroup 實(shí)現(xiàn)類(lèi)名

  // 生成 LogisticsCenter.register(name) 代碼

 mv.visitMethodInsn(Opcodes.INVOKESTATIC

 , ScanSetting.GENERATE_TO_CLASS_NAME

 , ScanSetting.REGISTER_METHOD_NAME

 , "(Ljava/lang/String;)V"

 , false)

 }

 }

 super.visitInsn(opcode)

 }

 @Override

 void visitMaxs(int maxStack, int maxLocals) {

 super.visitMaxs(maxStack + 4, maxLocals)

 }

}

最終章咧,生成的代碼會(huì)像下面所示:


private static void loadRouterMap() {

 registerByPlugin = false;

  //auto generate register code by gradle plugin: arouter-auto-register

  // looks like below:

 register("com.alibaba.android.arouter.routes.ARouter$$Root$$app");

 register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$app");

 register("com.alibaba.android.arouter.routes.ARouter$$Group$$arouter");

}

那么順便來(lái)跟蹤一下 register 方法的代碼,看看里面是如何完成路由表注冊(cè)的能真。


private static void register(String className) {

 if (!TextUtils.isEmpty(className)) {

 try {

 Class<?> clazz = Class.forName(className);

 Object obj = clazz.getConstructor().newInstance();

 if (obj instanceof IRouteRoot) {

 registerRouteRoot((IRouteRoot) obj);

 } else if (obj instanceof IProviderGroup) {

 registerProvider((IProviderGroup) obj);

 } else if (obj instanceof IInterceptorGroup) {

 registerInterceptor((IInterceptorGroup) obj);

 } else {

 logger.info(TAG, "register failed, class name: " + className

 + " should implements one of IRouteRoot/IProviderGroup/IInterceptorGroup.");

 }

 } catch (Exception e) {

 logger.error(TAG,"register class error:" + className);

 }

 }

}

// 注冊(cè) IRouteRoot 類(lèi)型

private static void registerRouteRoot(IRouteRoot routeRoot) {

 markRegisteredByPlugin();

 if (routeRoot != null) {

 routeRoot.loadInto(Warehouse.groupsIndex);

 }

}

// 注冊(cè) IInterceptorGroup 類(lèi)型

private static void registerInterceptor(IInterceptorGroup interceptorGroup) {

 markRegisteredByPlugin();

 if (interceptorGroup != null) {

 interceptorGroup.loadInto(Warehouse.interceptorsIndex);

 }

}

// 注冊(cè) IProviderGroup 類(lèi)型

private static void registerProvider(IProviderGroup providerGroup) {

 markRegisteredByPlugin();

 if (providerGroup != null) {

 providerGroup.loadInto(Warehouse.providersIndex);

 }

}

// 標(biāo)記通過(guò)gradle plugin完成自動(dòng)注冊(cè)

private static void markRegisteredByPlugin() {

 if (!registerByPlugin) {

 registerByPlugin = true;

 }

}

這樣相比之下赁严,自動(dòng)注冊(cè)的方式確實(shí)比掃描 dex 文件更高效,掃描 dex 文件是在 app 運(yùn)行時(shí)操作的舟陆,這樣會(huì)影響 app 的性能误澳,對(duì)用戶(hù)造成不好的體驗(yàn)。而自動(dòng)注冊(cè)是在 build 的時(shí)候完成字節(jié)碼插樁的秦躯,對(duì)運(yùn)行時(shí)不產(chǎn)生影響忆谓。

學(xué)了今天這招,以后 compiler 生成的代碼需要注冊(cè)的步驟都可以通過(guò)自動(dòng)注冊(cè)來(lái)完成了踱承,贊一個(gè)??

番外

====

之前看到自動(dòng)注冊(cè)這么神奇倡缠,所以想看下插入字節(jié)碼之后 LogisticsCenter 代碼的效果哨免,所以反編譯了一下 ARouter demo apk,可以看到 LogisticsCenter.smali 的 loadRouterMap 方法:


.method private static loadRouterMap()V

  .locals  1

 .line 64

 const/4 v0, 0x0

 sput-boolean v0, Lcom/alibaba/android/arouter/core/LogisticsCenter;->registerByPlugin:Z

 .line 69

  const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava"

 invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

  const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin"

 invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

  const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi"

 invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

  const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Root$$app"

 invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

  const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava"

 invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

  const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Interceptors$$app"

 invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

  const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Providers$$modulejava"

 invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

  const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Providers$$modulekotlin"

 invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

  const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Providers$$arouterapi"

 invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

  const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Providers$$app"

 invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

  return-void

.end method

確實(shí)符合我們的預(yù)期啊昙沦,真好琢唾!

References

==========

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市盾饮,隨后出現(xiàn)的幾起案子采桃,更是在濱河造成了極大的恐慌,老刑警劉巖丘损,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件普办,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡徘钥,警方通過(guò)查閱死者的電腦和手機(jī)衔蹲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)呈础,“玉大人舆驶,你說(shuō)我怎么就攤上這事《” “怎么了沙廉?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)笨忌。 經(jīng)常有香客問(wèn)我蓝仲,道長(zhǎng),這世上最難降的妖魔是什么官疲? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任袱结,我火速辦了婚禮,結(jié)果婚禮上途凫,老公的妹妹穿的比我還像新娘垢夹。我一直安慰自己,他們只是感情好维费,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布果元。 她就那樣靜靜地躺著,像睡著了一般犀盟。 火紅的嫁衣襯著肌膚如雪而晒。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,036評(píng)論 1 285
  • 那天阅畴,我揣著相機(jī)與錄音倡怎,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛监署,可吹牛的內(nèi)容都是我干的颤专。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼钠乏,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼栖秕!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起晓避,我...
    開(kāi)封第一講書(shū)人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤簇捍,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后够滑,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體垦写,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年彰触,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片命辖。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡况毅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出尔艇,到底是詐尸還是另有隱情尔许,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布终娃,位于F島的核電站味廊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏棠耕。R本人自食惡果不足惜余佛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望窍荧。 院中可真熱鬧辉巡,春花似錦、人聲如沸蕊退。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)瓤荔。三九已至净蚤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間输硝,已是汗流浹背今瀑。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人放椰。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓作烟,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親砾医。 傳聞我的和親對(duì)象是個(gè)殘疾皇子拿撩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容