首先乎折,現(xiàn)在世面上的項(xiàng)目基本上都是N多個(gè)module并行開發(fā)很容易就會(huì)出現(xiàn)moduleA想跳轉(zhuǎn)到moduleB某一個(gè)界面去如果你沒有把moduleB在對(duì)應(yīng)的build.gradle中配置的話凶杖,AS就會(huì)友好的提示你跳不過去媒楼,這時(shí)候就需要一個(gè)路由來分發(fā)跳轉(zhuǎn)操作了疚顷。
其次辙诞,隨著時(shí)間的慢慢迭代發(fā)現(xiàn)需求功能已經(jīng)寫完了,慢慢開始要各種優(yōu)化了线婚,常見的優(yōu)化是速度優(yōu)化自然而然就需要查看方法的耗時(shí)情況贿衍,那么解放雙手的時(shí)候就需要一個(gè)正確的姿勢(shì)來統(tǒng)計(jì)方法耗時(shí)。
附上Github項(xiàng)目地址:https://github.com/Neacy/NeacyPlugin
思路
1.采用注解(Annotation)在要跳轉(zhuǎn)的界面和需要統(tǒng)計(jì)的地方加上相對(duì)應(yīng)的協(xié)議牡直。
2.用groovy語(yǔ)言實(shí)現(xiàn)一個(gè)Transform的gradle插件來解析相對(duì)應(yīng)的注解缀匕。
3.采用ASM框架生成相對(duì)應(yīng)的代碼主要是寫入或者插入class的字節(jié)碼。
4.路由框架中需要反射拿到ASM生成的路由表然后代碼中調(diào)用從而實(shí)現(xiàn)跳轉(zhuǎn)碰逸。
==============帶著這些思路接下來就是拼命寫代碼了.............
先上兩個(gè)用到的注釋乡小,注釋還是比較簡(jiǎn)單的分分鐘寫完,需要注意的是我們是class操作所以要選@Retention(RetentionPolicy.CLASS)
/**
* 用于標(biāo)記協(xié)議
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface NeacyProtocol {
String value();
}
/**
* 用于標(biāo)記方法耗時(shí)
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface NeacyCost {
String value();
}
換個(gè)姿勢(shì)寫一個(gè)gradle插件饵史,如何寫主要參考區(qū)長(zhǎng)的http://blog.csdn.net/sbsujjbcy/article/details/50782830满钟,按著步驟就好,假設(shè)我們看完了并設(shè)置好了那么就有一個(gè)雛形了:
public class NeacyPlugin extends Transform implements Plugin<Project> {
private static final String PLUGIN_NAME = "NeacyPlugin"
private Project project
@Override
void apply(Project project) {
this.project = project
def android = project.extensions.getByType(AppExtension);
android.registerTransform(this)
}
@Override
String getName() {
return PLUGIN_NAME
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return true
}
@Override
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
}
}
我們要做的就是在transform中掃描相對(duì)應(yīng)的注解并用ASM寫入class字節(jié)碼胳喷。我們知道TransformInput對(duì)應(yīng)的有兩種可能性一種是目錄 一種是jar包所以要分開遍歷:
inputs.each { TransformInput input ->
input.directoryInputs.each { DirectoryInput directoryInput ->
if (directoryInput.file.isDirectory()) {
println "==== directoryInput.file = " + directoryInput.file
directoryInput.file.eachFileRecurse { File file ->
// ...對(duì)目錄進(jìn)行插入字節(jié)碼
}
}
//處理完輸入文件之后湃番,要把輸出給下一個(gè)任務(wù)
def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}
input.jarInputs.each { JarInput jarInput ->
println "------=== jarInput.file === " + jarInput.file.getAbsolutePath()
File tempFile = null
if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
// ...對(duì)jar進(jìn)行插入字節(jié)碼
}
/**
* 重名輸出文件,因?yàn)榭赡芡?會(huì)覆蓋
*/
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
//處理jar進(jìn)行字節(jié)碼注入處理
def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, dest)
}
}
對(duì)于代碼中陌生的代碼風(fēng)格可以查閱這篇文章:http://blog.csdn.net/innost/article/details/48228651保證看完之后什么都懂了,好文強(qiáng)烈推薦吭露。
然后吠撮,最麻煩的就是字節(jié)碼注入的部分功能了,先看一下主要的調(diào)用代碼:
ClassReader classReader = new ClassReader(file.bytes)
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
NeacyAsmVisitor classVisitor = new NeacyAsmVisitor(Opcodes.ASM5, classWriter)
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
調(diào)用的主要代碼量還是比較少的讲竿,主要是自定義一個(gè)ClassVisitor纬向。在每一個(gè)ClassVisitor中它會(huì)分別visitAnnotation
和visitMethod
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
NeacyLog.log("=====---------- NeacyAsmVisitor visitAnnotation ----------=====");
NeacyLog.log("=== visitAnnotation.desc === " + desc);
AnnotationVisitor annotationVisitor = super.visitAnnotation(desc, visible);
if (Type.getDescriptor(NeacyProtocol.class).equals(desc)) {// 如果注解不為空的話
mProtocolAnnotation = new NeacyAnnotationVisitor(Opcodes.ASM5, annotationVisitor, desc);
return mProtocolAnnotation;
}
return annotationVisitor;
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
NeacyLog.log("=====---------- visitMethod ----------=====");
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
mMethodVisitor = new NeacyMethodVisitor(Opcodes.ASM5, mv, access, name, desc);
return mMethodVisitor;
}
在visitAnnotation
中就是我們掃描相對(duì)應(yīng)的注解的地方類似Type.getDescriptor(NeacyProtocol.class).equals(desc)
判斷是否是我們需要的處理的注解,像這里我們主要處理前面定義好的注解NeacyProtocol
和NeacyCost
兩個(gè)注解就好戴卜。
這里我要展示一下注入成功之后的class中的代碼是什么模樣:
生成好的路由表:
注入成功的耗時(shí)代碼:
看一眼logcat打印出來的耗時(shí)時(shí)間逾条,感覺離成功不遠(yuǎn)了⊥栋可是是怎么注入的呢师脂,首先要看一眼class結(jié)構(gòu) 這里推薦使用IntelliJ IDEA然后裝個(gè)插件叫Bytecode outline這里距離看一眼耗時(shí)的生成的class文件字節(jié)碼。
左邊是我們對(duì)應(yīng)的java文件,右邊是編譯之后生成的class字節(jié)碼吃警。對(duì)于右邊一般是看不懂的但是神奇的ASM就能看的懂而且提供了一系列的api供我們調(diào)用糕篇,我們只要對(duì)著編寫就好了,按照上面的操作很大程度上減少了巨大的工作難度酌心,再次感謝巴掌大神拌消。
所以我們路由框架的代碼字節(jié)生成,我把整個(gè)類貼上來吧代碼量不是很多:
/**
* 生成路由class文件
*/
public class NeacyRouterWriter implements Opcodes {
public byte[] generateClass(String pkg, HashMap<String, String> metas) {
ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
// 生成class類標(biāo)識(shí)
cw.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, pkg, null, "java/lang/Object", null);
// 聲明一個(gè)靜態(tài)變量
fv = cw.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "map", "Ljava/util/HashMap;", "Ljava/util/HashMap<Ljava/lang/String;Ljava/lang/String;>;", null);
fv.visitEnd();
// 默認(rèn)的構(gòu)造函數(shù)<init>
mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(1, 1);
// 生成一個(gè)getMap方法
mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "getMap", "()Ljava/util/HashMap;", "()Ljava/util/HashMap<Ljava/lang/String;Ljava/lang/String;>;", null);
mv.visitCode();
mv.visitFieldInsn(Opcodes.GETSTATIC, pkg, "map", "Ljava/util/HashMap;");
mv.visitInsn(Opcodes.ARETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
// 將掃描到的注解生成相對(duì)應(yīng)的路由表 主要寫在靜態(tài)代碼塊中
mv = cw.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
mv.visitCode();
mv.visitTypeInsn(Opcodes.NEW, "java/util/HashMap");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/util/HashMap", "<init>", "()V", false);
mv.visitFieldInsn(Opcodes.PUTSTATIC, pkg, "map", "Ljava/util/HashMap;");
for (Map.Entry<String, String> entrySet : metas.entrySet()) {
String key = entrySet.getKey();
String value = entrySet.getValue();
NeacyLog.log("=== key === " + key);
NeacyLog.log("=== value === " + value);
mv.visitFieldInsn(Opcodes.GETSTATIC, pkg, "map", "Ljava/util/HashMap;");
mv.visitLdcInsn(key);
mv.visitLdcInsn(value);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/util/HashMap", "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", false);
mv.visitInsn(Opcodes.POP);
}
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(3, 0);
mv.visitEnd();
cw.visitEnd();
return cw.toByteArray();
}
}
然后對(duì)方法耗時(shí)的進(jìn)行的代碼插入主要代碼有:
@Override
protected void onMethodEnter() {
if (isInject) {
NeacyLog.log("====== 開始插入方法 = " + methodName);
/**
NeacyCostManager.addStartTime("xxxx", System.currentTimeMillis());
*/
mv.visitLdcInsn(methodName);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "neacy/router/NeacyCostManager", "addStartTime", "(Ljava/lang/String;J)V", false);
}
}
@Override
protected void onMethodExit(int opcode) {
if (isInject) {
/**
NeacyCostManager.addEndTime("xxxx", System.currentTimeMillis());
*/
mv.visitLdcInsn(methodName);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "neacy/router/NeacyCostManager", "addEndTime", "(Ljava/lang/String;J)V", false);
/**
NeacyCostManager.startCost("xxxx");
*/
mv.visitLdcInsn(methodName);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "neacy/router/NeacyCostManager", "startCost", "(Ljava/lang/String;)V", false);
NeacyLog.log("==== 插入結(jié)束 ====");
}
}
基本上這樣子相對(duì)應(yīng)的路由表相對(duì)應(yīng)的代碼插入都寫完安券,然后只需要在gradle插件中進(jìn)行調(diào)用一下即可墩崩,而對(duì)于遍歷目錄的時(shí)候沒有什么難點(diǎn)就是直接覆蓋當(dāng)前class即可:
if (isDebug) {// 只有Debug才進(jìn)行掃描const耗時(shí)
// 掃描耗時(shí)注解 NeacyCost
byte[] bytes = classWriter.toByteArray()
File destFile = new File(file.parentFile.absoluteFile, name)
project.logger.debug "========== 重新寫入的位置->lastFilePath = " + destFile.getAbsolutePath()
FileOutputStream fileOutputStream = new FileOutputStream(destFile)
fileOutputStream.write(bytes)
fileOutputStream.close()
}
而對(duì)于jar遍歷的時(shí)候需要做的是先拆jar然后注入代碼完成之后需要再生產(chǎn)一個(gè)jar,所以我們需要?jiǎng)?chuàng)建一個(gè)臨時(shí)地址來存放新的jar侯勉。
if (isDebug) {
// 將jar包解壓后重新打包的路徑
tempFile = new File(jarInput.file.getParent() + File.separator + "neacy_const.jar")
if (tempFile.exists()) {
tempFile.delete()
}
fos = new FileOutputStream(tempFile)
jarOutputStream = new JarOutputStream(fos)
// 省略一些代碼....
ZipEntry zipEntry = new ZipEntry(entryName)
jarOutputStream.putNextEntry(zipEntry)
// 掃描耗時(shí)注解 NeacyCost
byte[] bytes = classWriter.toByteArray()
jarOutputStream.write(bytes)
}
這里有必要插入一個(gè)插件配置鹦筹,因?yàn)閷?duì)于方法耗時(shí)統(tǒng)計(jì)只要開發(fā)的時(shí)候debug模式下使用就好其他模式禁止使用了,這就是為什么上面有if(debugOn)
的判斷址貌。
先定義一個(gè)Extension:
/**
* 配置
*/
public class NeacyExtension {
boolean debugOn = true
public NeacyExtension(Project project) {
}
}
然后在transfrom
中進(jìn)行讀阮砉铡:
void apply(Project project) {
this.project = project
project.extensions.create("neacy", NeacyExtension, project)
def android = project.extensions.getByType(AppExtension);
android.registerTransform(this)
project.afterEvaluate {
def extension = project.extensions.findByName("neacy") as NeacyExtension
def debugOn = extension.debugOn
project.logger.error '========= debugOn = ' + debugOn
project.android.applicationVariants.each { varient ->
project.logger.error '======== varient Name = ' + varient.name
if (varient.name.contains(DEBUG) && debugOn) {
isDebug = true
}
}
}
}
最后在build.gradle
中進(jìn)行配置就可以愉快的使用了..
apply plugin: com.neacy.plugin.NeacyPlugin
neacy {
debugOn true
}
當(dāng)然更多的代碼可以參考demo的git庫(kù)了解更多。
最后路由庫(kù)要怎么讓代碼調(diào)用呢练对,這就是前面講到的反射因?yàn)槭蔷幾g生成的class無法直接調(diào)用唯有反射大法遍蟋,反射會(huì)稍微影響性能所以我們一開始就直接做好這些初始化工作就可以了。
/**
* 初始化路由
*/
public void initRouter() {
try {
Class clazz = Class.forName("com.neacy.router.NeacyProtocolManager");
Object newInstance = clazz.newInstance();
Field field = clazz.getField("map");
field.setAccessible(true);
HashMap<String, String> temps = (HashMap<String, String>) field.get(newInstance);
if (temps != null && !temps.isEmpty()) {
mRouters.putAll(temps);
Log.w("Jayuchou", "=== mRouters.Size === " + mRouters.size());
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 根據(jù)協(xié)議找尋路由實(shí)現(xiàn)跳轉(zhuǎn)
*/
public void startIntent(Context context, String protocol, Bundle bundle) {
if (TextUtils.isEmpty(protocol)) return;
String protocolValue = mRouters.get(protocol);
try {
Class destClass = Class.forName(protocolValue);
Intent intent = new Intent(context, destClass);
if (bundle != null) {
intent.putExtras(bundle);
}
context.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
}
}
最最最后螟凭,怎么使用呢匿值?
@NeacyProtocol("Neacy://app/MainActivity")
public class MainActivity extends AppCompatActivity {
@Override
@NeacyCost("MainActivity.onCreate")
protected void onCreate(Bundle savedInstanceState) {
根據(jù)上面的注解標(biāo)識(shí)之后,方法耗時(shí)就已經(jīng)完成當(dāng)然路由還需要哪里需要哪里傳協(xié)議進(jìn)行跳轉(zhuǎn)就好了赂摆,當(dāng)然也是一句代碼的事挟憔。
NeacyRouterManager.getInstance().startIntent(TestActivity.this, "Neacy://neacymodule/NeacyModuleActivity", bundle);
這樣一個(gè)完整的路由框架以及方法耗時(shí)統(tǒng)計(jì)V1.0
版本就打完收工了。
Thanks............
感謝巴神的文章:http://www.wangyuwei.me/2017/03/05/ASM實(shí)戰(zhàn)統(tǒng)計(jì)方法耗時(shí)/#more