AOP中用到的术语
面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
Aspect :切面,一个关注点的模块化,这个关注点可能会横切多个对象。
Join Point :连接点,程序中可切入的点,例如方法调用时、读取某个变量时。
Pointcut :切入点,代码注入的位置。
Advice :在切入点注入的代码,一般有 before、after、around 三种类型。
Target Object :被一个或多个 aspect 横切拦截操作的目标对象。
Weaving : 把 Advice 代码织入到目标对象的过程。
Inter-type declarations : 用来给一个类型声明额外的方法或属性。
AspectJ 是在静态织入代码,即在编译期注入代码的。
一 AspectJ之热补丁技术
利用java的 AOP技术,对字节码进行处理。
1.构建时利用aop技术对每个方法进行插桩的操作
gradle进行构建的时候,在Java源码编译完成之后,生成dex文件之前,调用AspectJ的编译器进行插桩。插桩的目的是给每一个方法注入一段寻找patch方法的逻辑。 简单来说就是每个方法执行时先去根据自己方法的签名寻找是否有自己对应的patch方法,如果有执行patch方法,没有执行自己原有的逻辑。
怎么进行插桩
相关的AspectJ需要去了解。
@Aspectpublic class PatchAspect { private static final String TAG = "PatchAspect"; @Around("execution(* *..*()) && !withincode(* com.zpw.patch..*(..))") public void weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable { try { String e = joinPoint.toLongString(); //获取当前方法的签名 System.out.println("AspectJ拦截到的方法->" + e); if(PatchUtils.hasMethodPatch(e)) { //查询是否有自己方法的patch方法 Object target = joinPoint.getTarget(); //获取当前的运行对象,方便设置给patch方法 Object[] params = joinPoint.getArgs(); //获取运行参数 //在Application中获取patch包中需要打补丁的类 ListclassNameList = ZApplication.classNameList; if (classNameList != null && classNameList.size() > 0) { for (String className : classNameList) { Class patch = (Class) ZApplication.cl.loadClass(className); Object patchObj = patch.newInstance(); Method[] methods = patchObj.getClass().getMethods(); for (Method method : methods) { PatchAnnotation annotation = method.getAnnotation(PatchAnnotation.class); if (annotation != null) { System.out.println(); if (e.contains(annotation.value())) { method.setAccessible(true); method.invoke(patchObj); return; } } } } } joinPoint.proceed(); } } catch (Exception var8) { var8.printStackTrace(); } joinPoint.proceed(); }}复制代码
public class PatchUtils { private static final String TAG = "PatchUtils"; public static boolean hasMethodPatch(String methodSignature) throws ClassNotFoundException, IllegalAccessException, InstantiationException { ListclassNameList = ZApplication.classNameList; if (classNameList != null && classNameList.size() > 0) { for (String className : classNameList) { Class patch = null; try { patch = ZApplication.cl.loadClass(className); System.out.println("补丁中的类->" + patch.getName()); Object patchObj = patch.newInstance(); Method[] methods = patchObj.getClass().getMethods(); for (Method declaredMethod : methods) { System.out.println("补丁中的方法->" + declaredMethod.getName()); PatchAnnotation annotation = declaredMethod.getAnnotation(PatchAnnotation.class); if (annotation != null) { System.out.println("补丁中的方法的注解->" + annotation.value()); return true; } } } catch (Exception e) { e.printStackTrace(); } } } return false; }}复制代码
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface PatchAnnotation { String value(); String intercept();}复制代码
public class ZApplication extends Application { private static final String TAG = "ZApplication"; public static ListclassNameList = new ArrayList<>(); public static DexClassLoader cl; @Override public void onCreate() { super.onCreate(); StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder(); StrictMode.setVmPolicy(builder.build()); //拷贝补丁 copyData(); // 获取补丁,如果存在就执行注入操作 String dexInternalStoragePath = getDir("dex", Context.MODE_PRIVATE).getAbsolutePath().concat("/patch_dex.jar"); File file = new File(dexInternalStoragePath); String optimizedDexOutputPath = getDir("outdex", Context.MODE_PRIVATE).getAbsolutePath(); if (file.exists()) { try { inject(dexInternalStoragePath, optimizedDexOutputPath); } catch (IOException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } else { Log.e("ZApplication", dexInternalStoragePath + "不存在"); } cl = new DexClassLoader(dexInternalStoragePath, optimizedDexOutputPath, null, getClassLoader()); } /** * 要注入的dex的路径 * * @param dexInternalStoragePath * @param optimizedDexOutputPath */ private void inject(String dexInternalStoragePath, String optimizedDexOutputPath) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException { DexFile dexFile = DexFile.loadDex(dexInternalStoragePath, optimizedDexOutputPath, 0); if (dexFile != null) { Enumeration entries = dexFile.entries(); if (entries != null) { while (entries.hasMoreElements()) { String className = entries.nextElement(); System.out.println("补丁中的类->" + className); classNameList.add(className); } } else { System.out.println("entries is null!"); } } else { System.out.println("dexFile is null!"); } } void copyData() { InputStream in = null; FileOutputStream out = null; String path = getDir("dex", Context.MODE_PRIVATE).getAbsolutePath().concat("/patch_dex.jar"); // data/data目录 File file = new File(path); if (!file.exists()) { try { in = this.getAssets().open("patch_dex.jar"); // 从assets目录下复制 out = new FileOutputStream(file); int length = -1; byte[] buf = new byte[1024]; while ((length = in.read(buf)) != -1) { out.write(buf, 0, length); } out.flush(); } catch (Exception e) { e.printStackTrace(); } finally { if (in != null) { try { in.close(); } catch (IOException e1) { e1.printStackTrace(); } } if (out != null) { try { out.close(); } catch (IOException e1) { e1.printStackTrace(); } } } } }}复制代码
何时插桩
java源码编译完成之后,Dex文件生成之前。
如何基于构建插桩
1.hook java Compiler的Task,在java源码编译完成之后执行AspectJ的编译器,进行字节码插桩操作。
import org.aspectj.bridge.IMessageimport org.aspectj.bridge.MessageHandlerimport org.aspectj.tools.ajc.Mainfinal def log = project.loggerfinal def variants = project.android.applicationVariantsvariants.all { variant -> if (!variant.buildType.isDebuggable()) { log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.") return; } JavaCompile javaCompile = variant.javaCompile javaCompile.doLast { String[] args = ["-showWeaveInfo", "-1.5", "-inpath", javaCompile.destinationDir.toString(), "-aspectpath", javaCompile.classpath.asPath, "-d", javaCompile.destinationDir.toString(), "-classpath", javaCompile.classpath.asPath, "-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)] log.debug "ajc args: " + Arrays.toString(args) MessageHandler handler = new MessageHandler(true); new Main().run(args, handler); for (IMessage message : handler.getMessages(null, true)) { switch (message.getKind()) { case IMessage.ABORT: case IMessage.ERROR: case IMessage.FAIL: log.error message.message, message.thrown break; case IMessage.WARNING: log.warn message.message, message.thrown break; case IMessage.INFO: log.info message.message, message.thrown break; case IMessage.DEBUG: log.debug message.message, message.thrown break; } } }}复制代码
dependencies { compile 'org.aspectj:aspectjrt:1.8.1'}复制代码
2.gradle Transform API方案
对于上述方法存在一些问题,对于一些jar包,其实已经是字节码了不会走这个过程,因此对一些jar的插桩操作不是很好处理。因此我们采用了Transform API的方案。 transform api是Android gradle plugin 1.5之后新api, 作用就是在生成dex之前,给开发者一个机会能够统一进行修改字节码。 用这个方案写了一个实现了Transform API的gradle 插件,具体对哪些jar包进行插桩我们通过gradle配置的方式实现。这就大大的方便了我们对自己的代码和第三方的代码进行字节码的处理。
2.创建patch补丁包
假设错误类为
public class Animal { public void hurtThrows(){ int i = 4 / 0; }}复制代码
出现bug时将需要进行替换的方法放到指定类,然后生成一个只含有此java类字节码的apk包,进行下发
public class AnimalPatch{ private String TAG = "AnimalPatch"; @PatchAnnotation(intercept="after", value="public void com.zpw.hotfix.Animal.hurtThrows()") public void hurtThrowsFix() { System.out.println("hurtThrowsFix"); }}复制代码
3.补丁包的传输和加载
在补丁包加载之前一定要注意安全性校验、安全行校验、安全性校验。加载patch包的方式: 由于最终的patch包的形式是一个apk,因此加载也很简单直接使用android的DexFile.loadDex()将apk加载。加载完成之后遍历每一个Enumeration, 并反射获取所有class的Method,进行缓存起来,方便每个方法在缓存中查找。
二 检测方法耗时
1.注入逻辑
@Aspectpublic class AspectJSpectControler { @Around(value = "execution(* com.zpw.test..*.*(..)") public Object weavePatchLogic(ProceedingJoinPoint joinPoint) throws Throwable { if (BuildConfig.DEBUG) { //debug 状态下计算方法耗时 long startT = System.currentTimeMillis(); Object proceed = joinPoint.proceed(); long consume = System.currentTimeMillis() - startT; if (consume > 40 && Thread.currentThread().getId() == BaseApplication.getInstance().getMainThreadId()) { // 方法耗时大于40毫秒,并且当前线程是主线程,则直接打印当前方法的签名 NeteaseLog.d(METHOD_TIME_TAG, consume + " ms " + joinPoint.getSignature() + " main thread method"); } return proceed; } return joinPoint.proceed(); } }复制代码
2.运行app,过滤log查看方法耗时
adb logcat | grep method
三 对第三方sdk的某些方法进行hook修改
同理。
四 处理权限问题
同理。在需要判断权限的地方使用AspectJ进行代码注入。