前言
之前寫過Android全埋點解決方案(ASM 一 Transform),但是這個實際上只有transform,沒有asm相關(guān)的。它只是使用transform遍歷下文件而已。今天會使用到ASM做插樁。
一、ASM
是一個功能比較齊全的java字節(jié)碼操作與分析框架。通過使用ASM框架,我們可以動態(tài)生產(chǎn)類或者增強既有類的功能。ASM可以直接生成二進制.class文件,也可以在類被jvm加載前,動態(tài)的改變現(xiàn)有類的行為。Java的二進制被存儲在嚴格格式定義.class文件里面,這些字節(jié)碼文件擁有足夠的元數(shù)據(jù)信息用來表示類中的所有元素,包括名稱、方法、屬性以及java字節(jié)碼指令。ASM從字節(jié)碼文件中讀入這些信息后,能夠改變類的行為、分析類的信息,甚至能夠根據(jù)具體的要求生成新的類。
二、簡單介紹ASM幾個核心類
- ClassReader. 改類主要用來解析編譯過的.class字節(jié)碼文件
- ClassWriter 該類用來構(gòu)建重新編譯后的類,比如修改類的類名、方法、屬性,甚至是生成新的類字節(jié)碼文件。
- ClassVisitor. 主要負責“拜訪”類成員信息。其中包括表記在類上面的注解、類的構(gòu)造、類的字段、類的方法、靜態(tài)代碼塊等。
- AdviceAdapter 實現(xiàn)了MethodVisitor接口,主要負責拜訪方法的信息,用來進行具體的方法字節(jié)碼操作。
三、ASM+Transform 點擊事件插樁原理
我們可以自定義一個Gradle Plugin,然后注冊一個Transform對象。在transform方法里面可以分別遍歷目標和jar包,然后我們就可以遍歷當前應用程序所有的.class文件。然后再利用ASM框架的相關(guān)API,去加載相應的.class文件,就可以找到特定滿足特定條件的.class文件和相關(guān)方法,最后去修改相應的方法以動態(tài)插入埋點字節(jié)碼,從而達到自動埋點的效果。
四、實現(xiàn)
- 把埋點做成一個sdk,代碼在https://github.com/yangzai100/ASTDemo/tree/master 里面master分支的sdk中。然后依賴到主app中,并初始化。
- 創(chuàng)建一個android Library module,名稱叫:plugin
- 清空plugin.gradle,修改成如下內(nèi)容
apply plugin: 'groovy'
apply plugin: 'maven'
dependencies {
implementation gradleApi()
implementation localGroovy()
implementation 'org.ow2.asm:asm:7.1'
implementation 'org.ow2.asm:asm-commons:7.1'
// compile 'org.ow2.asm:asm-analysis:7.1'
// compile 'org.ow2.asm:asm-util:7.0'
// compile 'org.ow2.asm:asm-tree:7.1'
compileOnly 'com.android.tools.build:gradle:3.4.1'
}
repositories {
jcenter()
}
uploadArchives{
repositories.mavenDeployer{
//本地倉庫路徑,以放到項目根目錄下的repo的文件夾為列子
repository(url:uri('../repo'))
//groupId 自定定義
pom.groupId = "com.sensorsdata"
//artifactId
pom.artifactId = "autotrack.android"
//插件版本號
pom.version = "1.1.5"
}
}
創(chuàng)建groovy目錄
清空plugin/src/main目錄下所有的文件。然后在plugin/src下面創(chuàng)建groovy目錄,在里面創(chuàng)建一個package,比如com.sensorsdata.analytics.android.plugin新建Transform類. 代碼關(guān)鍵地方都有注釋
package com.sensorsdata.analytics.android.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.QualifiedContent
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformOutputProvider
import com.android.build.gradle.internal.pipeline.TransformManager
import groovy.io.FileType
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.gradle.api.Project
import com.android.build.api.transform.Transform
public class SensorAnalyticsTransform extends Transform{
private static Project project;
public SensorAnalyticsTransform(Project project) {
this.project = project;
}
@Override
String getName() {
return "sensorsAnalytics"
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs,
TransformOutputProvider outputProvider, boolean isIncremental) throws IOException,
TransformException, InterruptedException {
super.transform(context, inputs, referencedInputs, outputProvider, isIncremental)
print("我開始transform了")
if (!incremental){
outputProvider.deleteAll()
}
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<>()
/** 遍歷以某一擴展名結(jié)尾的文件*/
dir.traverse(type: FileType.FILES,nameFilter : ~/.*\.class/){
File classFile ->
/**排除sdk和support系統(tǒng)包 R相關(guān)的和系統(tǒng)相關(guān)的 提高編譯速度*/
if (SensorsAnalyticsClassModifier.isShouldModify(classFile.name)){
/**
* 修改.class文件,將修改后的.class文件放到一個HashMap中,然后將輸入目錄下的所有.class文件拷貝到輸出目錄,最后將
* HashMap中修改的.class文件拷貝到輸出目錄,覆蓋之前拷貝的.class文件(原.class文件)。*/
File modified = SensorsAnalyticsClassModifier.modifyClassFile(dir,classFile,context.getTemporaryDir())
if(modified != null){
/**key 為包名+類名
* 如:/cn/sensorsdata/autotrack/android/app/MainActivity.class
*/
String key = classFile.absolutePath.replace(dir.absolutePath,"")
modifyMap.put(key,modified)
}
}
}
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()
}
FileUtils.copyFile(en.getValue(),target)
en.getValue().delete()
}
}
}
input.jarInputs.each {
String destName = it.file.name
/**截取文件路徑對md5值重命名輸出文件,因為可能同名,會覆蓋*/
def hexName = DigestUtils.md5Hex(it.file.absolutePath).substring(0,8);
/*獲取jar名字*/
if(destName.endsWith(".jar")){
destName = destName.substring(0,destName.length() - 4)
}
/**獲取輸出文件*/
File dest = outputProvider.getContentLocation(destName + "_" + hexName,
it.contentTypes,it.scopes,Format.JAR)
def modifiedJar = SensorsAnalyticsClassModifier.modifyJar(it.file,
context.getTemporaryDir(),true)
if (modifiedJar == null){
modifiedJar = it.file
}
FileUtils.copyFile(modifiedJar,dest)
}
}
}
}
會用到SensorsAnalyticsClassModifier類
package com.sensorsdata.analytics.android.plugin
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.IOUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.regex.Matcher
class SensorsAnalyticsClassModifier {
private static HashSet<String> exclude = new HashSet<>();
static {
exclude = new HashSet<>();
exclude.add("android.support")
exclude.add("com.sensorsdata.analytics.android.sdk")
}
static File modifyJar(File jarFile, File tempDir, boolean nameHex) {
/**
* 讀取原 jar
*/
def file = new JarFile(jarFile, false)
/**
* 設置輸出到的 jar
*/
def hexName = ""
if (nameHex) {
hexName = DigestUtils.md5Hex(jarFile.absolutePath).substring(0, 8)
}
def outputJar = new File(tempDir, hexName + jarFile.name)
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(outputJar))
Enumeration enumeration = file.entries()
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement()
InputStream inputStream = null
try {
inputStream = file.getInputStream(jarEntry)
} catch (Exception e) {
return null
}
String entryName = jarEntry.getName()
if (entryName.endsWith(".DSA") || entryName.endsWith(".SF")) {
//ignore
} else {
String className
JarEntry jarEntry2 = new JarEntry(entryName)
jarOutputStream.putNextEntry(jarEntry2)
byte[] modifiedClassBytes = null
byte[] sourceClassBytes = IOUtils.toByteArray(inputStream)
if (entryName.endsWith(".class")) {
className = entryName.replace(Matcher.quoteReplacement(File.separator), ".").replace(".class", "")
if (isShouldModify(className)) {
modifiedClassBytes = modifyClass(sourceClassBytes)
}
}
if (modifiedClassBytes == null) {
modifiedClassBytes = sourceClassBytes
}
jarOutputStream.write(modifiedClassBytes)
jarOutputStream.closeEntry()
}
}
jarOutputStream.close()
file.close()
return outputJar
}
protected static boolean isShouldModify(String className) {
Iterator<String> iterator = exclude.iterator()
while (iterator.hasNext()) {
String packageName = iterator.next()
if (className.startsWith(packageName)) {
return false
}
}
if (className.contains('R$') || className.contains('R2$')
|| className.contains('R.class') || className.contains('R2.class')
|| className.contains('BuildConfig.class')) {
return false
}
return true
}
private static byte[] modifyClass(byte[] srcClass) {
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)
ClassVisitor classVisitor = new SensorsAnalyticsClassVisitor(classWriter)
ClassReader cr = new ClassReader(srcClass)
cr.accept(classVisitor, ClassReader.SKIP_FRAMES)
return classWriter.toByteArray()
}
/**
* 先獲取包名和類名,再獲取.class文件字節(jié)數(shù)組,調(diào)用modifyClass進行修改,再將修改后的byte數(shù)組生成.class文件
* @param dir
* @param classFile
* @param tempDir
* @return
*/
static File modifyClassFile(File dir, File classFile, File tempDir) {
File modify = null
try {
String className = path2className(classFile.absolutePath.replace(dir.absolutePath + File.separator, ""))
byte[] sourceClassBytes = IOUtils.toByteArray(new FileInputStream(classFile))
byte[] modifiedClassBytes = modifyClass(sourceClassBytes)
if (modifiedClassBytes) {
modify = new File(tempDir, className.replace(".", "") + ".class")
if (modify.exists())
modify.delete()
}
modify.createNewFile()
new FileOutputStream(modify).write(modifiedClassBytes)
} catch (Exception e) {
e.printStackTrace()
modify = classFile
}
return modify
}
static String path2className(String pathName) {
pathName.replace(File.separator, ".").replace(".class", "")
}
}
又會用到SensorsAnalyticsClassVisitor類
package com.sensorsdata.analytics.android.plugin
import org.objectweb.asm.AnnotationVisitor
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.Handle
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
class SensorsAnalyticsClassVisitor extends ClassVisitor implements Opcodes{
private final
static String SDK_API_CLASS = "com/sensorsdata/analytics/android/sdk/SensorsDataAutoTrackHelper"
private ClassVisitor classVisitor
private String[] mInterfaces
private HashMap<String, SensorsAnalyticsMethodCell> mLambdaMethodCells = new HashMap<>()
SensorsAnalyticsClassVisitor( ClassVisitor cv) {
super(Opcodes.ASM6, cv)
this.classVisitor = cv
}
///Classvisitor 掃描類的第一個調(diào)用的方法
@Override
void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces)
//version 表示jdk版本 例如51代碼jdk1.7
//access ACC_PUBLIC ACC_ 開頭是常量
//name 代表類的名稱 字節(jié)碼是以/表示路徑的:a/b/c/MyClass 也不需要寫.class
//signature 表示泛型,如果類沒有定義泛型,表示為null
//supername 表示當前類所繼承的父類。普通類我們雖然沒有寫父類,但是jdk編譯的時候會加上去
//interfaces 表示類所實現(xiàn)的接口列表
//visitorMethod 剛方法是當掃描器掃描到方法的時候調(diào)用
mInterfaces = interfaces
}
private
static void visitMethodWithLoadedParams(MethodVisitor methodVisitor, int opcode, String owner, String methodName, String methodDesc, int start, int count, List<Integer> paramOpcodes) {
for (int i = start; i < start + count; i++) {
methodVisitor.visitVarInsn(paramOpcodes[i - start], i)
}
methodVisitor.visitMethodInsn(opcode, owner, methodName, methodDesc, false)
}
@Override
MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
//accss方法修飾符
//name 表示方法名
//desc 表示方法簽名 舉例 String[] [Ljava/lang/String; Class<?> Ljava/lang/Class
//signature 表示泛型相關(guān)的信息
MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions)
String nameDesc = name + desc
methodVisitor = new SensorsAnalyticsDefaultMethodVisitor(methodVisitor, access, name, desc) {
boolean isSensorsDataTrackViewOnClickAnnotation = false
@Override
void visitEnd() {
super.visitEnd()
if (mLambdaMethodCells.containsKey(nameDesc)) {
mLambdaMethodCells.remove(nameDesc)
}
}
@Override
void visitInvokeDynamicInsn(String name1, String desc1, Handle bsm, Object... bsmArgs) {
super.visitInvokeDynamicInsn(name1, desc1, bsm, bsmArgs)
try {
String desc2 = (String) bsmArgs[0]
SensorsAnalyticsMethodCell sensorsAnalyticsMethodCell = SensorsAnalyticsHookConfig.LAMBDA_METHODS.get(Type.getReturnType(desc1).getDescriptor() + name1 + desc2)
if (sensorsAnalyticsMethodCell != null) {
Handle it = (Handle) bsmArgs[1]
mLambdaMethodCells.put(it.name + it.desc, sensorsAnalyticsMethodCell)
}
} catch (Exception e) {
e.printStackTrace()
}
}
/**
* 在原有的方法前面進行插樁
* 和他對應的有onMethodExit 在原有的方法后插樁
*/
@Override
protected void onMethodEnter() {
super.onMethodEnter()
/**mLambdaMethodCells
* 在 android.gradle 的 3.2.1 版本中,針對 view 的 setOnClickListener 方法 的 lambda 表達式做特殊處理。
*/
SensorsAnalyticsMethodCell lambdaMethodCell = mLambdaMethodCells.get(nameDesc)
if (lambdaMethodCell != null) {
Type[] types = Type.getArgumentTypes(lambdaMethodCell.desc)
int length = types.length
Type[] lambdaTypes = Type.getArgumentTypes(desc)
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
}
}
/**
* 包含OnclickListener 接口就做插樁 ,插入trackViewOnClick方法
*/
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)
}
}
}
@Override
AnnotationVisitor visitAnnotation(String s, boolean b) {
if (s == 'Lcom/sensorsdata/analytics/android/sdk/SensorsDataTrackViewOnClick;') {
isSensorsDataTrackViewOnClickAnnotation = true
}
return super.visitAnnotation(s, b)
}
}
return methodVisitor
}
/**
* 獲取方法參數(shù)下標為 index 的對應 ASM index
* @param types 方法參數(shù)類型數(shù)組
* @param index 方法中參數(shù)下標,從 0 開始
* @param isStaticMethod 該方法是否為靜態(tài)方法
* @return 訪問該方法的 index 位參數(shù)的 ASM index
*/
int getVisitPosition(Type[] types, int index, boolean isStaticMethod) {
if (types == null || index < 0 || index >= types.length) {
throw new Error("getVisitPosition error")
}
if (index == 0) {
return isStaticMethod ? 0 : 1
} else {
return getVisitPosition(types, index - 1, isStaticMethod) + types[index - 1].getSize()
}
}
}
- 自定義plugin來注冊transform,源碼如下
package com.sensorsdata.analytics.android.plugin
import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
public class SensorsAnalyticsPlugin implements Plugin<Project>{
//project ':app'
@Override
void apply(Project project) {
AppExtension appExtension = project.extensions.findByType(AppExtension.class)
appExtension.registerTransform(new SensorAnalyticsTransform(project))
}
}
- 新建proprties文件 讓系統(tǒng)找到Plugin
在plugin/src/main目錄下依次新建目錄resources/META-INF/gradle-plugins,然后在改目錄下新建文件com.sensorsdata.android.properties,其中com.sensorsdata.android就是我們的插件名稱。文件內(nèi)容如下:
implementation-class=com.sensorsdata.analytics.android.plugin.SensorsAnalyticsPlugin
構(gòu)建插件 ./gradlew uploadArchives命令構(gòu)建或者點擊android studio右邊的uploadArchives
-
添加對插件的依賴
在根gradle下面添加
根gradle.png
然后在app的gradle中
apply plugin: 'com.sensorsdata.android'
OK,到這里就全部弄完了。
build一下,在app下的build中查看
當然自己動手會遇到很多問題,例如groovy代碼編輯器根本不會提示,只能在upload和編譯時候才會報錯。
然后debug可以通過android stuido
斷點不進去可以通過clean后再去斷點。