字节码插桩(三): ASM 字节码插桩(2)

简介: 字节码插桩(三): ASM 字节码插桩(2)

1.3 ASM 原理

刚刚长篇大论说了ASM的使用以及简单API介绍,那么ASM实施过程是怎样的呢,主要是分为三个步骤

image.png

步骤一: 定义一个 Gradle Plugin 。 然后注册一个 Transform 对象。 在 transform 方法里,可以分别遍历 目录 和 jar 包

步骤二: 遍历当前应用程序所有的 .class文件,就可以找到满足特定条件的.class 文件和相关方法

步骤三: 修改相应方法以动态插入字节码


1.3 ASM 埋点方案

下面以自动采集Android 的 Button 空间的点击事件为例 ,纤细介绍该方案的实现步骤。对于其他控件点击事件有空再补充

第一步: 新建一个 Project

image.png

第二步: 创建 sdk module

image.png

第三步: 编写埋点SDK

/**
* @author 杨正友(小木箱)于 2020/10/9 20 19 创建
* @Email: yzy569015640@gmail.com
* @Tel: 18390833563
* @function description:
*/
@Keep
public class SensorsDataAPI {
   private final String TAG = this.getClass().getSimpleName();
   public static final String SDK_VERSION = "1.0.0";
   private static SensorsDataAPI INSTANCE;
   private static final Object  LOCK = new Object();
   private static Map<String, Object> mDeviceInfo;
   private String mDeviceId;
   @Keep
   @SuppressWarnings("UnusedReturnValue")
   public static SensorsDataAPI init(Application application) {
       synchronized (LOCK) {
           if (null == INSTANCE) {
               INSTANCE = new SensorsDataAPI(application);
           }
           return INSTANCE;
       }
   }
   @Keep
   public static SensorsDataAPI getInstance() {
       return INSTANCE;
   }
   private SensorsDataAPI(Application application) {
       mDeviceId = SensorsDataPrivate.getAndroidID(application.getApplicationContext());
       mDeviceInfo = SensorsDataPrivate.getDeviceInfo(application.getApplicationContext());
   }
   /**
    * Track 事件
    *
    * @param eventName  String 事件名称
    * @param properties JSONObject 事件属性
    */
   @Keep
   public void track(@NonNull final String eventName, @Nullable JSONObject properties) {
       try {
           JSONObject jsonObject = new JSONObject();
           jsonObject.put("event", eventName);
           jsonObject.put("device_id", mDeviceId);
           JSONObject sendProperties = new JSONObject(mDeviceInfo);
           if (properties != null) {
               SensorsDataPrivate.mergeJSONObject(properties, sendProperties);
           }
           jsonObject.put("properties", sendProperties);
           jsonObject.put("time", System.currentTimeMillis());
           Log.i(TAG, SensorsDataPrivate.formatJson(jsonObject.toString()));
       } catch (Exception e) {
           e.printStackTrace();
       }
   }
}

第四步: 在sdk module里新建 AutoTrackHelper.java 工具类

我们新增trackViewOnClick(View view),主要是ASM插入埋点代码

   /**
    * View 被点击,自动埋点
    *
    * @param view View
    */
   @Keep
   public static void trackViewOnClick(View view) {
       try {
           JSONObject jsonObject = new JSONObject();
           jsonObject.put("$element_type", SensorsDataPrivate.getElementType(view));
           jsonObject.put("$element_id", SensorsDataPrivate.getViewId(view));
           jsonObject.put("$element_content", SensorsDataPrivate.getElementContent(view));
           Activity activity = SensorsDataPrivate.getActivityFromView(view);
           if (activity != null) {
               jsonObject.put("$activity", activity.getClass().getCanonicalName());
           }
           SensorsDataAPI.getInstance().track("$AppClick", jsonObject);
       } catch (Exception e) {
           e.printStackTrace();
       }
   }

第五步: 添加依赖关系

image.png

第六步: 初始化埋点SDK

image.png

第七步: 声明自定义的Application

image.png

第八步: 新建一个Android Lib 叫做 Plugin

image.png

第九步: 清空 build.gradle 文件的内容,然后修改如下内容

image.png

第十步: 创建 groovy 目录

image.png

第十一步: 新建 Transform 目录

/**
* @author 杨正友(小木箱)于 2020/10/9 22 08 创建
* @Email: yzy569015640@gmail.com
* @Tel: 18390833563
* @function description:
*/
class MkAnalyticsTransform extends Transform {
   private static Project project
   private MkAnalyticsExtension sensorsAnalyticsExtension
   MkAnalyticsTransform(Project project, MkAnalyticsExtension sensorsAnalyticsExtension) {
       this.project = project
       this.sensorsAnalyticsExtension = sensorsAnalyticsExtension
   }
   @Override
   String getName() {
       return "MkAnalytics"
   }
   /**
    * 需要处理的数据类型,有两种枚举类型
    * CLASSES 代表处理的 java 的 class 文件,RESOURCES 代表要处理 java 的资源
    * @return
    */
   @Override
   Set<QualifiedContent.ContentType> getInputTypes() {
       return TransformManager.CONTENT_CLASS
   }
   /**
    * 指 Transform 要操作内容的范围,官方文档 Scope 有 7 种类型:
    * 1. EXTERNAL_LIBRARIES        只有外部库
    * 2. PROJECT                   只有项目内容
    * 3. PROJECT_LOCAL_DEPS        只有项目的本地依赖(本地jar)
    * 4. PROVIDED_ONLY             只提供本地或远程依赖项
    * 5. SUB_PROJECTS              只有子项目。
    * 6. SUB_PROJECTS_LOCAL_DEPS   只有子项目的本地依赖项(本地jar)。
    * 7. TESTED_CODE               由当前变量(包括依赖项)测试的代码
    * @return
    */
   @Override
   Set<QualifiedContent.Scope> getScopes() {
       return TransformManager.SCOPE_FULL_PROJECT
   }
   @Override
   boolean isIncremental() {
       return false
   }
   @Override
   void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
       _transform(transformInvocation.context, transformInvocation.inputs, transformInvocation.outputProvider, transformInvocation.incremental)
   }
   void _transform(Context context, Collection<TransformInput> inputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
       if (!incremental) {
           outputProvider.deleteAll()
       }
       /**Transform 的 inputs 有两种类型,一种是目录,一种是 jar 包,要分开遍历 */
       inputs.each { TransformInput input ->
           /**遍历目录*/
           input.directoryInputs.each { DirectoryInput directoryInput ->
               /**当前这个 Transform 输出目录*/
               File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
               File dir = directoryInput.file
               if (dir) {
                   HashMap<String, File> modifyMap = new HashMap<>()
                   /**遍历以某一扩展名结尾的文件*/
                   dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) {
                       File classFile ->
                           if (MkAnalyticsClassModifier.isShouldModify(classFile.name)) {
                               File modified = null
                               if (!sensorsAnalyticsExtension.disableAppClick) {
                                   modified = MkAnalyticsClassModifier.modifyClassFile(dir, classFile, context.getTemporaryDir())
                               }
                               if (modified != null) {
                                   /**key 为包名 + 类名,如:/cn/sensorsdata/autotrack/android/app/MainActivity.class*/
                                   String ke = classFile.absolutePath.replace(dir.absolutePath, "")
                                   modifyMap.put(ke, modified)
                               }
                           }
                   }
                   // 将输入目录下的所有 .class 文件 拷贝到输出目录
                   FileUtils.copyDirectory(directoryInput.file, dest)
                   modifyMap.entrySet().each {
                       Map.Entry<String, File> en ->
                           File target = new File(dest.absolutePath + en.getKey())
                           if (target.exists()) {
                               target.delete()
                           }
                           // 将HashMap 中修改过的 .class 文件拷贝到输出目录,覆盖之前拷贝的 .class 文件(原 .class文件)
                           FileUtils.copyFile(en.getValue(), target)
                           en.getValue().delete()
                   }
               }
           }
           /**遍历 jar*/
           input.jarInputs.each { JarInput jarInput ->
               String destName = jarInput.file.name
               /**截取文件路径的 md5 值重命名输出文件,因为可能同名,会覆盖*/
               def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath).substring(0, 8)
               /** 获取 jar 名字*/
               if (destName.endsWith(".jar")) {
                   destName = destName.substring(0, destName.length() - 4)
               }
               /** 获得输出文件*/
               File dest = outputProvider.getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
               def modifiedJar = null;
               if (!sensorsAnalyticsExtension.disableAppClick) {
                   modifiedJar = MkAnalyticsClassModifier.modifyJar(jarInput.file, context.getTemporaryDir(), true)
               }
               if (modifiedJar == null) {
                   modifiedJar = jarInput.file
               }
               FileUtils.copyFile(modifiedJar, dest)
           }
       }
   }
}

MkAnalyticsTransform 继承 Transform 。 在 Transform 里, 会分别遍历 目录和 jar。实现的相关抽象方法,与之前我们实现的 Gradle Transform 样例一致,具体的话可以跳回去看上文介绍。

A. 遍历目录

分别遍历目录里面每一个 .class 文件,首先通过MkAnalyticsClassModifier.isShouldModify 方法简单过滤一下肯定不需要的 .class 文件。 isShouldModify 方法实现逻辑比较简单

  // 将修改的 .class 文件放到一个HashMap对象中
   private static HashSet<String> exclude = new HashSet<>();
   static {
       exclude = new HashSet<>()
       // 过滤.class文件1: android.support 包下的文件
       exclude.add('android.support')
       // 过滤.class文件2: 我们sdk下的.class文件
      exclude.add('com.github.microkibaco.asm_sdk')
   }
   /**
    * 判断是否需要修改
    * @param className 类对象
    * @return boolean
    */
   protected static boolean isShouldModify(String className) {
       Iterator<String> iterator = exclude.iterator()
       while (iterator.hasNext()) {
           String packageName = iterator.next()
           // 提高编译效率
           if (className.startsWith(packageName)) {
               return false
           }
       }
       // 过滤.class文件3: R.class 及其子类
       if (className.contains('R$') ||
               // 过滤.class文件4: R2.class 及其子类
               className.contains('R2$') ||
               className.contains('R.class') ||
               className.contains('R2.class') ||
               // 过滤.class文件5: BuildConfig.class
               className.contains('BuildConfig.class')) {
           return false
       }
       return true
   }

比如我们可以简单过滤 如下: .class 文件

  • android.supoort包下的文件
  • 我们 SDK 的 .class 文文件
  • R.classs 及其子类
  • R2.class 及其子类(ButterKnife生成)
  • BuildConfig.class

之所以要过滤一些文件,主要是为了提高编译效率。

B. 遍历 jar

image.png

第十二步: 定义 Plugin

image.png

第十三步: 新建properites 文件

image.png

第十四步: 构建插件

image.png

第十五步: 添加对插件的依赖

image.png

第十六步: 在应用程序使用插件

image.png

第十七步: 构建应用程序

确认app/build/intermediates/transforms/MknalyticeAutoTrack/debug/是否有生成新的 .class 文件 有没有插入新的字节码

1.5 ASM 存在的风险点

无法采集android:onClick 属性绑定的点击事件

第一步: 新增一个注解 @SensorsDataTrckViewOnClick

image.png

第二步: visitMethod 注解标记

在前面定义的 MethodVistor 类里, 有一个叫 visitMethod 方法,该方法是扫描到方法注解声明的时进行调用。判断一下当前扫描到的注解是否为我们自定义的注解类型,如果是则做个标记,然后在 visitMethod 判断是否有这个标记,如果有,则埋点字节码。visitAnnotation 的实现如下:

image.png

第三步: visitAnnotation 注解特殊处理

在 visitAnnotation 方法里,我们判断一下当前扫描的注解(即第一个参数 s)是否是我们自定义的 @SensorsDataTrckViewOnClick 注解类型,如果是,就做个标记,即

image.png

第四步:  SensorsDataTrckViewHelper.tacakViewOnClick(view)  插入字节码

在onMethodExit方法里,如果 isSensorsDataTrackViewOnClickAnnotation 为 true,则说明该方法加了  @SensorsDataTrckViewOnClick 注解。如果被注解的方法有且只有一个View类型的参数,那我们就插入埋点代码,即插入代码SensorsDataTrckViewHelper.tacakViewOnClick(view) 对应的字节码

image.png

最后在 android: onClick 属性绑定方法上使用我们自定义的注解标记一下,即 @SensorsDataTrckViewOnClick

1.6 总结

本文 以App打包流程为基石 引入了 Gradle Transform ,并手把手教大家写了一个简单的 Transform Demo,通过 ASM +Gradle Transform 实现了Button全埋点组件,让大家更好理解 ASM 原理,当然该埋点组件存在一些不足,如: 不支持 AlerDialog MenuItem CheckBox SeekBar Spinner RattingBar TabHost ListView GridView 和 ExpendableListView 这个需要大家去扩展。ASM优势不用多说,实际开发中一般可用于大图监测,卡顿时间精准测量,日志上报等等。可以说是完美填补了AspectJ的不足。

相关文章
|
安全 算法 Java
从零开发基于ASM字节码的Java代码混淆插件XHood
因在公司负责基础框架的开发设计,所以针对框架源代码的保护工作比较重视,之前也加入了一系列保护措施,例如自定义classloader加密保护,授权license保护等,但都是防君子不防小人,安全等级还比较低,经过调研各类加密混淆措施后,决定自研混淆插件,自主可控,能够贴合实际情况进行定制化,达到框架升级后使用零感知,零影响
145 1
从零开发基于ASM字节码的Java代码混淆插件XHood
|
6月前
|
Java API Android开发
ASM 框架:字节码操作的常见用法(生成类,修改类,方法插桩,方法注入)
ASM 框架:字节码操作的常见用法(生成类,修改类,方法插桩,方法注入)
113 0
|
6月前
|
存储 算法 Java
ASM字节码操纵框架实现AOP
ASM字节码操纵框架实现AOP
62 0
|
6月前
|
存储 算法 Java
Android 进阶——代码插桩必知必会&ASM7字节码操作
Android 进阶——代码插桩必知必会&ASM7字节码操作
296 0
|
6月前
|
Java Kotlin
ASM字节码插桩实现点击防抖
ASM字节码插桩实现点击防抖
50 0
|
开发框架 Java Maven
SpringBoot自定义maven-plugin插件整合asm代码插桩
公司开发框架增加了web系统license授权证书校验模块,实行一台机器一个授权证书,初步方案是增加拦截器针对全局请求进行拦截校验,评估后认为校验方式单一,应该增加重要工具类,业务service实现中每个方法的进行校验,因为涉及代码量较大硬编码工作困难,故选择通过自定义maven插件在编译期间进行动态代码插桩操作
150 1
|
存储 算法 Java
一起来学字节码插桩:ASM Tree API
`ASM`是一个通用的`Java字节码操作和分析框架`。它可用于`修改现有类`或`直接以二进制形式动态生成类`。`ASM`提供了一些常见的字节码转换和分析算法,可以根据这些算法构建定制的复杂转换和代码分析工具。
292 0
|
7月前
|
Oracle 关系型数据库
oracle asm 磁盘显示offline
oracle asm 磁盘显示offline
353 2
|
2月前
|
存储 Oracle 关系型数据库
数据库数据恢复—Oracle ASM磁盘组故障数据恢复案例
Oracle数据库数据恢复环境&故障: Oracle ASM磁盘组由4块磁盘组成。Oracle ASM磁盘组掉线 ,ASM实例不能mount。 Oracle数据库故障分析&恢复方案: 数据库数据恢复工程师对组成ASM磁盘组的磁盘进行分析。对ASM元数据进行分析发现ASM存储元数据损坏,导致磁盘组无法挂载。
|
7月前
|
存储 Oracle 关系型数据库
【数据库数据恢复】Oracle数据库ASM磁盘组掉线的数据恢复案例
oracle数据库ASM磁盘组掉线,ASM实例不能挂载。数据库管理员尝试修复数据库,但是没有成功。
【数据库数据恢复】Oracle数据库ASM磁盘组掉线的数据恢复案例