1.3 ASM 原理
刚刚长篇大论说了ASM的使用以及简单API介绍,那么ASM实施过程是怎样的呢,主要是分为三个步骤
步骤一: 定义一个 Gradle Plugin 。 然后注册一个 Transform 对象。 在 transform 方法里,可以分别遍历 目录 和 jar 包
步骤二: 遍历当前应用程序所有的 .class文件,就可以找到满足特定条件的.class 文件和相关方法
步骤三: 修改相应方法以动态插入字节码
1.3 ASM 埋点方案
下面以自动采集Android 的 Button 空间的点击事件为例 ,纤细介绍该方案的实现步骤。对于其他控件点击事件有空再补充
第一步: 新建一个 Project
第二步: 创建 sdk module
第三步: 编写埋点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(); } }
第五步: 添加依赖关系
第六步: 初始化埋点SDK
第七步: 声明自定义的Application
第八步: 新建一个Android Lib 叫做 Plugin
第九步: 清空 build.gradle 文件的内容,然后修改如下内容
第十步: 创建 groovy 目录
第十一步: 新建 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
第十二步: 定义 Plugin
第十三步: 新建properites 文件
第十四步: 构建插件
第十五步: 添加对插件的依赖
第十六步: 在应用程序使用插件
第十七步: 构建应用程序
确认app/build/intermediates/transforms/MknalyticeAutoTrack/debug/是否有生成新的 .class 文件 有没有插入新的字节码
1.5 ASM 存在的风险点
无法采集android:onClick 属性绑定的点击事件
第一步: 新增一个注解 @SensorsDataTrckViewOnClick
第二步: visitMethod 注解标记
在前面定义的 MethodVistor 类里, 有一个叫 visitMethod 方法,该方法是扫描到方法注解声明的时进行调用。判断一下当前扫描到的注解是否为我们自定义的注解类型,如果是则做个标记,然后在 visitMethod 判断是否有这个标记,如果有,则埋点字节码。visitAnnotation 的实现如下:
第三步: visitAnnotation 注解特殊处理
在 visitAnnotation 方法里,我们判断一下当前扫描的注解(即第一个参数 s)是否是我们自定义的 @SensorsDataTrckViewOnClick 注解类型,如果是,就做个标记,即
第四步: SensorsDataTrckViewHelper.tacakViewOnClick(view) 插入字节码
在onMethodExit方法里,如果 isSensorsDataTrackViewOnClickAnnotation 为 true,则说明该方法加了 @SensorsDataTrckViewOnClick 注解。如果被注解的方法有且只有一个View类型的参数,那我们就插入埋点代码,即插入代码SensorsDataTrckViewHelper.tacakViewOnClick(view) 对应的字节码
最后在 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的不足。