前言
上篇文章我寫了入門篇:Gradle 插件 + ASM 實(shí)戰(zhàn)——入門篇,對gradle+ASM不熟的大家可以去上篇文章查看
-
Gradle Transform
Gradle Transform是Android官方提供給開發(fā)者在項(xiàng)目構(gòu)建階段(.class -> .dex轉(zhuǎn)換期間)用來修改.class文件的一套標(biāo)準(zhǔn)API,即把輸入的.class文件轉(zhuǎn)變成目標(biāo)字節(jié)碼文件
ClassVisitor
訪問類的成員信息
模板搭建
- 修改訪問入口BuryPointPlugin
package com.peakmain.analytics.plugin
import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
class BuryPointPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
BuryPointExtension extension = project.extensions.create("peakmainPlugin", BuryPointExtension)
boolean disableBuryPointPlugin = false
Properties properties = new Properties()
//gradle.properties是否存在
if(project.rootProject.file('gradle.properties').exists()){
//gradle.properties文件->輸入流
properties.load(project.rootProject.file('gradle.properties').newDataInputStream())
disableBuryPointPlugin=Boolean.parseBoolean(properties.getProperty("peakmainPlugin.disableAppClick","false"))
}
//如果disableBuryPointPlugin可用
if(!disableBuryPointPlugin){
AppExtension appExtension = project.extensions.findByType(AppExtension.class)
appExtension.registerTransform(new BuryPointTransform(project,extension))
}else{
println("------------您已關(guān)閉了埋點(diǎn)插件--------------")
}
}
}
- BuryPointTransform
package com.peakmain.analytics.plugin
import com.android.build.api.transform.Context
import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.Format
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformInvocation
import com.android.build.api.transform.TransformOutputProvider
import com.android.build.gradle.internal.pipeline.TransformManager
import org.objectweb.asm.ClassVisitor
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import org.gradle.api.Project
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
class BuryPointTransform extends Transform {
private static Project project
private BuryPointExtension buryPointExtension
BuryPointTransform(Project project, BuryPointExtension buryPointExtension) {
this.project = project
this.buryPointExtension = buryPointExtension
}
@Override
String getName() {
return "BuryPoint"
}
/**
* 需要處理的數(shù)據(jù)類型网严,有兩種枚舉類型
* CLASS->處理的java的class文件
* RESOURCES->處理java的資源
* @return
*/
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
/**
* 指 Transform 要操作內(nèi)容的范圍婉陷,官方文檔 Scope 有 7 種類型:
* 1. EXTERNAL_LIBRARIES 只有外部庫
* 2. PROJECT 只有項(xiàng)目內(nèi)容
* 3. PROJECT_LOCAL_DEPS 只有項(xiàng)目的本地依賴(本地jar)
* 4. PROVIDED_ONLY 只提供本地或遠(yuǎn)程依賴項(xiàng)
* 5. SUB_PROJECTS 只有子項(xiàng)目肛冶。
* 6. SUB_PROJECTS_LOCAL_DEPS 只有子項(xiàng)目的本地依賴項(xiàng)(本地jar)敷扫。
* 7. TESTED_CODE 由當(dāng)前變量(包括依賴項(xiàng))測試的代碼
* @return
*/
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
/**
* 是否增量編譯
* @return
*/
@Override
boolean isIncremental() {
return false
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
_transform(transformInvocation.context, transformInvocation.inputs, transformInvocation.outputProvider)
}
/**
*
* @param context
* @param inputs 有兩種類型秕衙,一種是目錄,一種是 jar 包掷匠,要分開遍歷
* @param outputProvider 輸出路徑
*/
void _transform(Context context, Collection<TransformInput> inputs, TransformOutputProvider outputProvider) throws IOException, TransformException, InterruptedException {
if (!incremental) {
//不是增量更新刪除所有的outputProvider
outputProvider.deleteAll()
}
inputs.each { TransformInput input ->
//遍歷目錄
input.directoryInputs.each { DirectoryInput directoryInput ->
handleDirectoryInput(directoryInput, outputProvider)
}
// 遍歷jar 第三方引入的 class
input.jarInputs.each { JarInput jarInput ->
handleJarInput(jarInput, outputProvider)
}
}
}
void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
if (directoryInput.file.isDirectory()) {
directoryInput.file.eachFileRecurse { File file ->
String name = file.name
if (filterClass(name)) {
// 用來讀 class 信息
ClassReader classReader = new ClassReader(file.bytes)
// 用來寫
ClassWriter classWriter = new ClassWriter(0 /* flags */)
//todo 改這里就可以了
ClassVisitor classVisitor = new BuryPointVisitor(classWriter)
// 下面還可以包多層
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
// 重新覆蓋寫入文件
byte[] code = classWriter.toByteArray()
FileOutputStream fos = new FileOutputStream(
file.parentFile.absolutePath + File.separator + name)
fos.write(code)
fos.close()
}
}
}
// 把修改好的數(shù)據(jù)滥崩,寫入到 output
def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes,
directoryInput.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}
void handleJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {
if (jarInput.file.absolutePath.endsWith(".jar")) {
// 重名名輸出文件,因?yàn)榭赡芡?會覆蓋
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
JarFile jarFile = new JarFile(jarInput.file)
Enumeration enumeration = jarFile.entries()
File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
if (tmpFile.exists()) {
tmpFile.delete()
}
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
//用于保存
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement()
String entryName = jarEntry.getName()
ZipEntry zipEntry = new ZipEntry(entryName)
InputStream inputStream = jarFile.getInputStream(jarEntry)
//插樁class
if (filterClass(entryName)) {
//class文件處理
jarOutputStream.putNextEntry(zipEntry)
ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
ClassWriter classWriter = new ClassWriter(0)
//todo 改這里就可以了
ClassVisitor classVisitor = new BuryPointVisitor(classWriter)
// 下面還可以包多層
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
jarOutputStream.write(code)
} else {
jarOutputStream.putNextEntry(zipEntry)
jarOutputStream.write(IOUtils.toByteArray(inputStream))
}
jarOutputStream.closeEntry()
}
//結(jié)束
jarOutputStream.close()
jarFile.close()
def dest = outputProvider.getContentLocation(jarName + md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(tmpFile, dest)
tmpFile.delete()
}
}
boolean filterClass(String className) {
return (className.endsWith(".class") && !className.startsWith("R\$")
&& "R.class" != className && "BuildConfig.class" != className)
}
}
不同項(xiàng)目只需要TODO位置就可以了
- BuryPointVisitor
class BuryPointVisitor extends ClassVisitor {
private ClassVisitor classVisitor
private String[] mInterfaces
BuryPointVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM6, classVisitor)
this.classVisitor = classVisitor
}
/**
* 掃描類的時(shí)候進(jìn)入這里
* @param version 類版本
* @param access 修飾符
* @param name 類名
* @param signature 泛型信息
* @param superName 父類
* @param interfaces 實(shí)現(xiàn)的接口
*/
@Override
void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
println("name->"+name+",superName->"+superName)
super.visit(version, access, name, signature, superName, interfaces)
this.mInterfaces=interfaces
}
}
-
編譯結(jié)果
ClassVisitor方法詳解
- visit方法:掃描類的時(shí)候會進(jìn)入這里,它的作用:可以替換一些類讹语,比如ImageView
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
if(superName.equals("android/widget/ImageView")
&& !name.equals("com/peakmain/PeakmainImageView")){
superName = "com/peakmain/PeakmainImageView";
}
super.visit(version, access, name, signature, superName, interfaces);
}
- visitMethod:掃描到方法的時(shí)候調(diào)用
/**
* 掃描類的方法進(jìn)行調(diào)用
* @param access 修飾符
* @param name 方法名字
* @param descriptor 方法簽名
* @param signature 泛型信息
* @param exceptions 拋出的異常
* @return
*/
@Override
MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
println("name->"+name+"----------------------descriptor->"+descriptor)
return methodVisitor
}
visitVarInsn
aload 0 相當(dāng)于字節(jié)碼mv.visitVarInsn(ALOAD, 0);加載局部變量表下標(biāo)0位置對象到操作數(shù)棧visitMethodInsn
執(zhí)行方法
private final static String SDK_API_CLASS = "com/peakmain/sdk/SensorsDataAutoTrackHelper"
methodVisitor.visitVarInsn(ALOAD, 1)
methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/view/View;)V", false)
等價(jià)于:執(zhí)行SensorsDataAutoTrackHelper的靜態(tài)方法trackViewOnClick參數(shù)是View view
- onMethodEnter
方法執(zhí)行之前插入字節(jié)碼代碼
protected void onMethodEnter() {
super.onMethodEnter()
if(name == "sendMessageAtTime"){
println("進(jìn)入sendMessageAtTime")
methodVisitor.visitLdcInsn("TAG");
methodVisitor.visitLdcInsn("PeakmainHandler->sendMessageAtTime")
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
}
}
相當(dāng)于在SendMessageAtTime方法之前插入了
Log.e("TAG", "PeakmainHandler->sendMessageAtTime");
- visitInsn:方法return返回之前插入字節(jié)碼,
- visitCode:方法調(diào)用前插入字節(jié)碼,并在onMethodEnter之后插入字節(jié)碼
實(shí)戰(zhàn)
注解方式獲取方法消耗的時(shí)間
private void getMessageStartCostTime(MethodVisitor methodVisitor) {
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
methodVisitor.visitVarInsn(LSTORE, 1)
Label label1 = new Label()
methodVisitor.visitLabel(label1)
}
private void getMessageEndCostTime(MethodVisitor methodVisitor, String name) {
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
methodVisitor.visitVarInsn(LLOAD, 1)
methodVisitor.visitInsn(LSUB)
methodVisitor.visitVarInsn(LSTORE, 2)
Label label2 = new Label();
methodVisitor.visitLabel(label2)
methodVisitor.visitLdcInsn("LogMessageCostTime")
methodVisitor.visitTypeInsn(NEW, "java/lang/StringBuilder");
methodVisitor.visitInsn(DUP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false)
methodVisitor.visitLdcInsn(name + "消耗的時(shí)間:")
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
methodVisitor.visitVarInsn(LLOAD, 2)
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false)
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false)
methodVisitor.visitInsn(POP);
Label label3 = new Label()
methodVisitor.visitLabel(label3)
}
編譯之后結(jié)果:
實(shí)現(xiàn)點(diǎn)擊事件的埋點(diǎn)
這里只截取部分代碼钙皮,完整代碼可查看github
protected void onMethodEnter() {
super.onMethodEnter()
/**
* 在 android.gradle 的 3.2.1 版本中,針對 view 的 setOnClickListener 方法 的 lambda 表達(dá)式做特殊處理。
*/
BuryPointMethodCell lambdaMethodCell = mMethodCells.get(nameDesc)
if (lambdaMethodCell != null) {
Type[] types = Type.getArgumentTypes(lambdaMethodCell.desc)
int length = types.length
Type[] lambdaTypes = Type.getArgumentTypes(descriptor)
int paramStart = lambdaTypes.length - length
if (paramStart < 0) {
return
} else {
for (int i = 0; i < length; i++) {
if (lambdaTypes[paramStart + i].descriptor != types[i].descriptor) {
return
}
}
}
boolean isStaticMethod = SensorsAnalyticsUtils.isStatic(access)
if (!isStaticMethod) {
if (lambdaMethodCell.desc == '(Landroid/view/MenuItem;)Z') {
methodVisitor.visitVarInsn(ALOAD, 0)
methodVisitor.visitVarInsn(ALOAD, getVisitPosition(lambdaTypes, paramStart, isStaticMethod))
methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, lambdaMethodCell.agentName, '(Ljava/lang/Object;Landroid/view/MenuItem;)V', false)
return
}
}
for (int i = paramStart; i < paramStart + lambdaMethodCell.paramsCount; i++) {
methodVisitor.visitVarInsn(lambdaMethodCell.opcodes.get(i - paramStart), getVisitPosition(lambdaTypes, i, isStaticMethod))
}
methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, lambdaMethodCell.agentName, lambdaMethodCell.agentDesc, false)
return
}
if (nameDesc == 'onContextItemSelected(Landroid/view/MenuItem;)Z' ||
nameDesc == 'onOptionsItemSelected(Landroid/view/MenuItem;)Z') {
methodVisitor.visitVarInsn(ALOAD, 0)
methodVisitor.visitVarInsn(ALOAD, 1)
methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Ljava/lang/Object;Landroid/view/MenuItem;)V", false)
}
if (isSensorsDataTrackViewOnClickAnnotation) {
if (desc == '(Landroid/view/View;)V') {
methodVisitor.visitVarInsn(ALOAD, 1)
methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/view/View;)V", false)
return
}
}
if ((mInterfaces != null && mInterfaces.length > 0)) {
if ((mInterfaces.contains('android/view/View$OnClickListener') && nameDesc == 'onClick(Landroid/view/View;)V')) {
methodVisitor.visitVarInsn(ALOAD, 1)
methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/view/View;)V", false)
} else if (mInterfaces.contains('android/content/DialogInterface$OnClickListener') && nameDesc == 'onClick(Landroid/content/DialogInterface;I)V') {
methodVisitor.visitVarInsn(ALOAD, 1)
methodVisitor.visitVarInsn(ILOAD, 2)
methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/content/DialogInterface;I)V", false)
} else if (mInterfaces.contains('android/content/DialogInterface$OnMultiChoiceClickListener') && nameDesc == 'onClick(Landroid/content/DialogInterface;IZ)V') {
methodVisitor.visitVarInsn(ALOAD, 1)
methodVisitor.visitVarInsn(ILOAD, 2)
methodVisitor.visitVarInsn(ILOAD, 3)
methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/content/DialogInterface;IZ)V", false)
} else if (mInterfaces.contains('android/widget/CompoundButton$OnCheckedChangeListener') && nameDesc == 'onCheckedChanged(Landroid/widget/CompoundButton;Z)V') {
methodVisitor.visitVarInsn(ALOAD, 1)
methodVisitor.visitVarInsn(ILOAD, 2)
methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/widget/CompoundButton;Z)V", false)
} else if (mInterfaces.contains('android/widget/RatingBar$OnRatingBarChangeListener') && nameDesc == 'onRatingChanged(Landroid/widget/RatingBar;FZ)V') {
methodVisitor.visitVarInsn(ALOAD, 1)
methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/view/View;)V", false)
} else if (mInterfaces.contains('android/widget/SeekBar$OnSeekBarChangeListener') && nameDesc == 'onStopTrackingTouch(Landroid/widget/SeekBar;)V') {
methodVisitor.visitVarInsn(ALOAD, 1)
methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/view/View;)V", false)
} else if (mInterfaces.contains('android/widget/AdapterView$OnItemSelectedListener') && nameDesc == 'onItemSelected(Landroid/widget/AdapterView;Landroid/view/View;IJ)V') {
methodVisitor.visitVarInsn(ALOAD, 1)
methodVisitor.visitVarInsn(ALOAD, 2)
methodVisitor.visitVarInsn(ILOAD, 3)
methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/widget/AdapterView;Landroid/view/View;I)V", false)
} else if (mInterfaces.contains('android/widget/TabHost$OnTabChangeListener') && nameDesc == 'onTabChanged(Ljava/lang/String;)V') {
methodVisitor.visitVarInsn(ALOAD, 1)
methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackTabHost", "(Ljava/lang/String;)V", false)
} else if (mInterfaces.contains('android/widget/AdapterView$OnItemClickListener') && nameDesc == 'onItemClick(Landroid/widget/AdapterView;Landroid/view/View;IJ)V') {
methodVisitor.visitVarInsn(ALOAD, 1)
methodVisitor.visitVarInsn(ALOAD, 2)
methodVisitor.visitVarInsn(ILOAD, 3)
methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/widget/AdapterView;Landroid/view/View;I)V", false)
} else if (mInterfaces.contains('android/widget/ExpandableListView$OnGroupClickListener') && nameDesc == 'onGroupClick(Landroid/widget/ExpandableListView;Landroid/view/View;IJ)Z') {
methodVisitor.visitVarInsn(ALOAD, 1)
methodVisitor.visitVarInsn(ALOAD, 2)
methodVisitor.visitVarInsn(ILOAD, 3)
methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackExpandableListViewGroupOnClick", "(Landroid/widget/ExpandableListView;Landroid/view/View;I)V", false)
} else if (mInterfaces.contains('android/widget/ExpandableListView$OnChildClickListener') && nameDesc == 'onChildClick(Landroid/widget/ExpandableListView;Landroid/view/View;IIJ)Z') {
methodVisitor.visitVarInsn(ALOAD, 1)
methodVisitor.visitVarInsn(ALOAD, 2)
methodVisitor.visitVarInsn(ILOAD, 3)
methodVisitor.visitVarInsn(ILOAD, 4)
methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackExpandableListViewChildOnClick", "(Landroid/widget/ExpandableListView;Landroid/view/View;II)V", false)
}
}
}
- 參考書籍:《Android全埋點(diǎn)解決方案》