1.需求分析
2.功能模块划分
3.基础工具类、框架、接口提取
4.创建路由接口
//路由基础工具类
public class RouterTools {
private static volatile RouterTools mRouterTools;
private static HashMap<String, Class<? extends Activity>> mRouterMap;
private RouterTools() {
mRouterMap = new HashMap<>();
}
public static RouterTools getInstance() {
if (mRouterTools == null) {
synchronized (RouterTools.class) {
if (mRouterTools == null) {
mRouterTools = new RouterTools();
}
}
}
return mRouterTools;
}
//添加路由与Activity对应
public void addRouter(String router, Class<? extends Activity> clazz) {
mRouterMap.put(router, clazz);
}
public void navigate(Context context, String router) {
context.startActivity(new Intent(context, mRouterMap.get(router)));
}
}
RouterTools.getInstance().addRouter("A/AMainActviity",AMainActivity.class);
RouterTools.getInstance().addRouter("B/AMainActviity",BMainActivity.class);
RouterTools.getInstance().addRouter("C/AMainActviity",CMainActivity.class);
...
...
...
//使用
RouterTools.getInstance().navigate(context,"A/AMainActviity");
虽然路由功能实现类,但是初始化过程比较复杂,使用过程需要收到addRouter对应关系,对开发者不友好
compileOnly 'com.google.auto.service:auto-service:1.0-rc7'
kapt'com.google.auto.service:auto-service:1.0-rc7'
implementation 'com.squareup:javapoet:1.12.1'
//注册注解器
@AutoService(Processor.class)
//添加要监听对注解
@SupportedAnnotationTypes({"com.example.annotation.MyRouter"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
//监听模块Options 实现模块分包
@SupportedOptions({"MODULE_NAME"})
public class MyRouterProcessor extends AbstractProcessor {
}
defaultConfig {
...
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [MODULE_NAME: project.getName()]
}
}
}
dependencies {
kapt project(':annotation')
}
@MyRouter("appb/BMainActivity")
class BMainActivity : AppCompatActivity() {
}
public class MyRouter$$APPC implements IRouter {
/**
* addRouter
* @routerMap 路由map
*/
@Override
public void addRouter(HashMap<String, Class<? extends android.app.Activity>> routerMap) {
routerMap.put("appc/CMainActivity",com.zhanpple.appc.CMainActivity.class);
}
}
//需要将对应模块接口传递给baseMoudlue
RouterTools.getInstance().init(new MyRouter$$APPA(),new MyRouter$$APPB(),new MyRouter$$APPC()...)
//基础模块
public class RouterTools{
public void init(IRouter... iRouters) {
for (IRouter iRouter : iRouters) {
iRouter.addRouter(mRouterMap);
}
}
}
优化后可通过注解生成对应的路由class键值对,但是初始化依然需要手动添加对应模块接口
//获取对象apk 遍历对应dex文件下的class,反射调用addRouter
public void init(Context context) {
String packageResourcePath = context.getApplicationContext().getPackageResourcePath();
try {
DexFile dexFile = new DexFile(packageResourcePath);
Enumeration<String> entries = dexFile.entries();
while (entries.hasMoreElements()) {
String element = entries.nextElement();
Log.e("RouterTools", "init: " + element);
if (element.contains("com.example.router")) {
Log.e("RouterTools", "init: " + element);
Class<?> aClass = Class.forName(element);
int modifiers = aClass.getModifiers();
if (IRouter.class.isAssignableFrom(aClass) && !Modifier.isInterface(modifiers)) {
((IRouter) aClass.newInstance()).addRouter(mRouterMap);
}
}
}
} catch (Exception e) {
e.printStackTrace();
Log.e("RouterTools", "init: ", e);
}
初始化
RouterTools.getInstance().init(context)
public void init(Context context) {
loadRouterMap();
if (isUseAms) {//如果调用了register方法就不需要遍历dex
return;
}
....
....
}
private static void loadRouterMap() {
isUseAms = false;
//在此处完成字节码插装
//register("com.example.router.appc.MyRouter$$APPC")
//register("com.example.router.appb.MyRouter$$APPB")
}
//提供给字节码插桩调用
private static void register(String className) {
isUseAms = true;
Log.e("RouterTools", "register: " + className);
try {
Class<?> aClass = Class.forName(className);
int modifiers = aClass.getModifiers();
if (IRouter.class.isAssignableFrom(aClass) && !Modifier.isInterface(modifiers)) {
((IRouter) aClass.newInstance()).addRouter(mRouterMap);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public class MyPlugin implements Plugin<Project> {
@Override
public void apply(@NotNull Project project) {
Logger.make(project);
Logger.w("apply:init-----------------");
boolean hasAppPlugin = project.getPlugins().hasPlugin(AppPlugin.class);
Logger.w("apply:init-----------------" + hasAppPlugin);
if (hasAppPlugin) {
Logger.w("apply:init-----------------" + hasAppPlugin);
AppExtension android = project.getExtensions().getByType(AppExtension.class);
//register this plugin
android.registerTransform(new MyRouterTransform(project));
}
}
}
apply plugin: 'java-library'
apply plugin: 'kotlin'
apply plugin: 'maven'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation gradleApi()
implementation 'com.android.tools.build:gradle:4.0.1'
}
apply plugin: 'maven-publish'
publishing {
publications {
mavenJava(MavenPublication) {
groupId 'com.example.android'
artifactId "zhanpple"
version 1.0
from components.java
// more goes in here
}
}
repositories {
mavenLocal()
}
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
package com.example.plugin.transform;
import com.example.plugin.tools.Logger;
import com.example.plugin.tools.ScanUtil;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.codehaus.groovy.antlr.treewalker.VisitorAdapter;
import org.gradle.api.Project;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;
/**
* Created at 9:41 2020/9/23
*
* @author zmp
* <p>
* des:定义自己的Transform
*/
public class MyRouterTransform extends MyBaseTransform {
public MyRouterTransform(Project project) {
super(project);
}
//扫描获取注解类和要插桩的工具类
protected void transformJar(File input, File dest) {
if (ScanUtil.shouldProcessPreDexJar(input.getAbsolutePath())) {
ScanUtil.scanJar(input, dest);
} else {
super.transformJar(input, dest);
}
}
//扫描获取注解类和要插桩的工具类
protected void transformSingleFile(String parentPath, File input, File dest) {
if (!parentPath.endsWith(File.separator)) {
parentPath += File.separator;
}
String replace = input.getAbsolutePath().replace(parentPath, "");
if (!File.separator.equals("/")) {
replace = replace.replaceAll("\\\\", "/");
}
try {
if (ScanUtil.shouldProcessClass(replace)) {
ScanUtil.scanClass(input, dest);
}
FileUtils.copyFile(input, dest);
} catch (Exception e) {
e.printStackTrace();
}
}
//扫描获取注解类和要插桩的工具类
protected void insertInitCodeTo() {
File containsInitClass = ScanUtil.fileContainsInitClass;
Logger.e("containsInitClass:" + containsInitClass);
Logger.e("containsInitClass:" + Arrays.toString(ScanUtil.CLASS_NAMES.toArray()));
if (containsInitClass != null) {
insertInitCodeIntoJarFile(containsInitClass);
}
}
/**
* generate code into jar file
*
* @param jarFile the jar file which contains LogisticsCenter.class
* @return
*/
private void insertInitCodeIntoJarFile(File jarFile) {
try {
File optJar = new File(jarFile.getParent(), jarFile.getName() + ".opt");
if (optJar.exists()) {
optJar.delete();
}
JarFile file = new JarFile(jarFile);
Enumeration<JarEntry> enumeration = file.entries();
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optJar));
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);
if (ScanUtil.ASM_CLASS_NAME.equals(entryName)) {
byte[] bytes = referHackWhenInit(inputStream);
jarOutputStream.write(bytes);
} else {
jarOutputStream.write(IOUtils.toByteArray(inputStream));
}
inputStream.close();
jarOutputStream.closeEntry();
}
jarOutputStream.close();
file.close();
if (jarFile.exists()) {
jarFile.delete();
}
optJar.renameTo(jarFile);
} catch (Exception e) {
e.printStackTrace();
}
}
//refer hack class when object init
private byte[] referHackWhenInit(InputStream inputStream) {
Logger.e("referHackWhenInit");
byte[] bytes = null;
try {
ClassReader cr = new ClassReader(inputStream);
ClassWriter cw = new ClassWriter(cr, 0);
ClassVisitor cv = new MyClassVisitor(Opcodes.ASM5, cw);
cr.accept(cv, ClassReader.EXPAND_FRAMES);
bytes = cw.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return bytes;
}
class MyClassVisitor extends ClassVisitor {
MyClassVisitor(int api, ClassVisitor cv) {
super(api, cv);
}
@Override
public void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
//generate code into this method
Logger.e("visitMethod:" + name);
if (name.equals(ScanUtil.GENERATE_TO_METHOD_NAME)) {
mv = new RouteMethodVisitor(api, mv, access, name, desc);
}
return mv;
}
}
static class RouteMethodVisitor extends AdviceAdapter {
/**
* Constructs a new {@link AdviceAdapter}.
*
* @param api the ASM API version implemented by this visitor. Must be one of {@link
* Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}.
* @param methodVisitor the method visitor to which this adapter delegates calls.
* @param access the method's access flags (see {@link Opcodes}).
* @param name the method's name.
* @param descriptor the method's descriptor (see Type).
*/
protected RouteMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}
@Override
protected void onMethodEnter() {
super.onMethodEnter();
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
//在loadRouterMap里插入代码
for (String className : ScanUtil.CLASS_NAMES) {
push(className);
invokeStatic(Type.getType(ScanUtil.ASM_CLASS_NAME_TYPE), new Method("register", "(Ljava/lang/String;)V"));
}
}
//防止栈溢出 maxStack + Integer.MAX_VALUE
@Override
public void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(maxStack + Integer.MAX_VALUE, maxLocals);
}
}
}
package com.example.plugin.tools;
import org.apache.commons.io.FileUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
public class ScanUtil {
public static final String FIND_CLASS_PACKAGE_NAME = "com/example/router";
public static final String INTERFACE_NAME = "com/example/basemoudle/IRouter";
public static final String ASM_CLASS_NAME = "com/example/basemoudle/RouterTools.class";
public static final String ASM_CLASS_NAME_TYPE = "Lcom/example/basemoudle/RouterTools;";
public static final ArrayList<String> CLASS_NAMES = new ArrayList<>();
public static final String GENERATE_TO_METHOD_NAME = "loadRouterMap";
public static File fileContainsInitClass;
/**
* scan jar file
*
* @param jarFile All jar files that are compiled into apk
* @param destFile dest file after this transform
*/
public static void scanJar(File jarFile, File destFile) {
try {
JarFile file = new JarFile(jarFile);
Enumeration<JarEntry> enumeration = file.entries();
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement();
String entryName = jarEntry.getName();
if (entryName.startsWith(FIND_CLASS_PACKAGE_NAME)) {
Logger.w("scanJar-----------------" + jarEntry);
InputStream inputStream = file.getInputStream(jarEntry);
scanClass(inputStream);
inputStream.close();
} else if (ASM_CLASS_NAME.equals(entryName)) {
//找到要插桩的类
fileContainsInitClass = destFile;
} else {
Logger.w("scanJar:" + entryName);
}
}
file.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
FileUtils.copyFile(jarFile, destFile);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static boolean shouldProcessPreDexJar(String path) {
return !path.contains("com.android.support") && !path.contains("/android/m2repository");
}
public static boolean shouldProcessClass(String entryName) {
Logger.e("entryName:" + entryName);
return entryName != null && entryName.endsWith(".class") && entryName.startsWith(FIND_CLASS_PACKAGE_NAME);
}
/**
* scan class file
*
* @param file class
*/
public static void scanClass(File file, File desFile) {
if (file.getAbsolutePath().contains("com/example/basemoudle/RouterTools")) {
fileContainsInitClass = desFile;
return;
}
try {
scanClass(new FileInputStream(file));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
private static void scanClass(InputStream inputStream) {
try {
ClassReader cr = new ClassReader(inputStream);
ClassWriter cw = new ClassWriter(cr, 0);
ScanClassVisitor cv = new ScanClassVisitor(Opcodes.ASM7, cw);
cr.accept(cv, ClassReader.EXPAND_FRAMES);
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static class ScanClassVisitor extends ClassVisitor {
ScanClassVisitor(int api, ClassVisitor cv) {
super(api, cv);
}
@Override
public void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
if (interfaces != null) {
for (String anInterface : interfaces) {
if (anInterface.equals(INTERFACE_NAME)) {
//fix repeated inject init code when Multi-channel packaging
if (!CLASS_NAMES.contains(name)) {
CLASS_NAMES.add(name);
Logger.w("add:------------------:" + name);
}
}
}
}
}
}
}