前言
之前写过Android全埋点解决方案(ASM 一 Transform),但是这个实际上只有transform,没有asm相关的。它只是使用transform遍历下文件而已。今天会使用到ASM做插桩。
一、ASM
是一个功能比较齐全的java字节码操作与分析框架。通过使用ASM框架,我们可以动态生产类或者增强既有类的功能。ASM可以直接生成二进制.class文件,也可以在类被jvm加载前,动态的改变现有类的行为。Java的二进制被存储在严格格式定义.class文件里面,这些字节码文件拥有足够的元数据信息用来表示类中的所有元素,包括名称、方法、属性以及java字节码指令。ASM从字节码文件中读入这些信息后,能够改变类的行为、分析类的信息,甚至能够根据具体的要求生成新的类。
二、简单介绍ASM几个核心类
- ClassReader. 改类主要用来解析编译过的.class字节码文件
- ClassWriter 该类用来构建重新编译后的类,比如修改类的类名、方法、属性,甚至是生成新的类字节码文件。
- ClassVisitor. 主要负责“拜访”类成员信息。其中包括表记在类上面的注解、类的构造、类的字段、类的方法、静态代码块等。
- AdviceAdapter 实现了MethodVisitor接口,主要负责拜访方法的信息,用来进行具体的方法字节码操作。
三、ASM+Transform 点击事件插桩原理
我们可以自定义一个Gradle Plugin,然后注册一个Transform对象。在transform方法里面可以分别遍历目标和jar包,然后我们就可以遍历当前应用程序所有的.class文件。然后再利用ASM框架的相关API,去加载相应的.class文件,就可以找到特定满足特定条件的.class文件和相关方法,最后去修改相应的方法以动态插入埋点字节码,从而达到自动埋点的效果。
四、实现
- 把埋点做成一个sdk,代码在https://github.com/yangzai100/ASTDemo/tree/master 里面master分支的sdk中。然后依赖到主app中,并初始化。
- 创建一个android Library module,名称叫:plugin
- 清空plugin.gradle,修改成如下内容
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"
}
}
创建groovy目录
清空plugin/src/main目录下所有的文件。然后在plugin/src下面创建groovy目录,在里面创建一个package,比如com.sensorsdata.analytics.android.plugin新建Transform类. 代码关键地方都有注释
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 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 inputs, Collection 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 modifyMap = new HashMap<>()
/** 遍历以某一扩展名结尾的文件*/
dir.traverse(type: FileType.FILES,nameFilter : ~/.*\.class/){
File classFile ->
/**排除sdk和support系统包 R相关的和系统相关的 提高编译速度*/
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 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 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 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文件字节数组,调用modifyClass进行修改,再将修改后的byte数组生成.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 mLambdaMethodCells = new HashMap<>()
SensorsAnalyticsClassVisitor( ClassVisitor cv) {
super(Opcodes.ASM6, cv)
this.classVisitor = cv
}
///Classvisitor 扫描类的第一个调用的方法
@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 代表类的名称 字节码是以/表示路径的:a/b/c/MyClass 也不需要写.class
//signature 表示泛型,如果类没有定义泛型,表示为null
//supername 表示当前类所继承的父类。普通类我们虽然没有写父类,但是jdk编译的时候会加上去
//interfaces 表示类所实现的接口列表
//visitorMethod 刚方法是当扫描器扫描到方法的时候调用
mInterfaces = interfaces
}
private
static void visitMethodWithLoadedParams(MethodVisitor methodVisitor, int opcode, String owner, String methodName, String methodDesc, int start, int count, List 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 表示泛型相关的信息
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
}
/**
* 获取方法参数下标为 index 的对应 ASM index
* @param types 方法参数类型数组
* @param index 方法中参数下标,从 0 开始
* @param isStaticMethod 该方法是否为静态方法
* @return 访问该方法的 index 位参数的 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 ':app'
@Override
void apply(Project project) {
AppExtension appExtension = project.extensions.findByType(AppExtension.class)
appExtension.registerTransform(new SensorAnalyticsTransform(project))
}
}
- 新建proprties文件 让系统找到Plugin
在plugin/src/main目录下依次新建目录resources/META-INF/gradle-plugins,然后在改目录下新建文件com.sensorsdata.android.properties,其中com.sensorsdata.android就是我们的插件名称。文件内容如下:
implementation-class=com.sensorsdata.analytics.android.plugin.SensorsAnalyticsPlugin
构建插件 ./gradlew uploadArchives命令构建或者点击android studio右边的uploadArchives
-
添加对插件的依赖
在根gradle下面添加
然后在app的gradle中
apply plugin: 'com.sensorsdata.android'
OK,到这里就全部弄完了。
build一下,在app下的build中查看
当然自己动手会遇到很多问题,例如groovy代码编辑器根本不会提示,只能在upload和编译时候才会报错。
然后debug可以通过android stuido
断点不进去可以通过clean后再去断点。