├── .gitignore ├── README.md ├── ams-plugin ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── java │ └── com │ │ └── tzx │ │ └── ams │ │ └── plugin │ │ ├── AmsClassVisitor.java │ │ ├── AmsConfig.java │ │ ├── AmsMethodVisitor.java │ │ ├── AmsPlugin.java │ │ ├── AmsTransform.java │ │ └── Logger.java │ └── resources │ └── META-INF │ └── gradle-plugins │ └── amsplugin.properties ├── ams ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── tzx │ │ └── ams │ │ ├── MethodEventManager.java │ │ ├── MethodObserver.java │ │ └── TrackMethod.java │ └── res │ ├── drawable │ └── ams_test.png │ └── values │ └── strings.xml ├── app ├── .gitignore ├── amsfilterclass.text ├── amsmethods.text ├── build.gradle ├── libs │ └── AmsTestJar_V1.0.jar ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── tzx │ │ └── amsdemo │ │ ├── App.java │ │ ├── MainActivity.java │ │ └── TimeObserver.java │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ └── activity_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── properties.png ├── repo.png ├── settings.gradle └── uploadArchives.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | .idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # AndroidPluginStudy 4 | 5 | [ASM](https://asm.ow2.io/) 是一个通用的`Java`字节码操作和分析框架。它可以直接以二进制形式用于修改现有类或动态生成类。 `ASM`提供了一些常见的字节码转换和分析算法,可以从中构建定制的复杂转换和代码分析工具。 `ASM`提供与其他`Java字`节码框架类似的功能,但侧重于性能。因为它的设计和实现是尽可能的小和尽可能快,所以它非常适合在动态系统中使用(但当然也可以以静态方式使用,例如在编译器中)。 6 | 7 | 8 | 9 | ## 自定义plugin开发 10 | 11 | `Gradle`从1.5开始,`Gradle`插件包含了一个叫`Transform`的API,这个API允许第三方插件在`class`文件转为为`dex`文件前操作编译好的`class`文件,这个API的目标是简化自定义类操作,而不必处理`Task`,并且在操作上提供更大的灵活性。并且可以更加灵活地进行操作。官方文档:http://google.github.io/android-gradle-dsl/javadoc/ 12 | 13 | ### 创建插件 14 | 15 | 在 `AndroidStudio` 中创建一个纯净的`Module` ,删除 `res` 文件夹和 `AndroidManifest.xml` 文件。 16 | 17 | **Gradle会默认在配置初始化前,编译buildSrc这个项目,可以是java、groovy、kotlin项目,并把项目配置到classpath下。**所以如果`Module` 命名为 `buildSrc` 那么不需要在 `Project` 级别的 `build.gradle` 文件中使用 `classpath` 引入,也不需要在 `app` 级别的 `build.gradle` 文件中使用 `apply plugin:` 进行应用。 18 | 19 | 下面我们介绍的是自定义的 `Plugin` 插件,是编译之后需要引入项目的。 20 | 21 | > 类继承`Plugin` 22 | 23 | ```java 24 | public class AmsPlugin implements Plugin { 25 | @Override 26 | public void apply(@NonNull Project project) { 27 | AppExtension appExtension = project.getExtensions().findByType(AppExtension.class); 28 | assert appExtension != null; 29 | //注册task任务 30 | appExtension.registerTransform(new AmsTransform(project)); 31 | } 32 | } 33 | public class AmsTransform extends Transform{ 34 | /***部分代码省略***/ 35 | } 36 | ``` 37 | 38 | > `build.gradle` 文件配置 39 | 40 | ![](./uploadArchives.png) 41 | 42 | > 配置`properties` 43 | 44 | 在 `/src/main/resources/META-INF/gradle-plugins` 目录下创建`xxx.properties` 文件,这个 `xxx` 就是以后项目`build.gradle` 文件中需要`apply` 的插件名称。 45 | 46 | ```properties 47 | implementation-class=com.tzx.ams.plugin.AmsPlugin 48 | ``` 49 | 50 | `com.tzx.ams.plugin.AmsPlugin` 就是插件对应的类。 51 | 52 | ![properties](./properties.png) 53 | 54 | ### 生成插件 55 | 56 | 我们在执行 `uploadArchives` 的任务的时候就在我们对于的仓库生成了我们需要的插件。 57 | 58 | ![](./repo.png) 59 | 60 | ### 在项目中引入插件 61 | 62 | > 项目的根 `build.gradle`配置 63 | 64 | ```groovy 65 | buildscript { 66 | repositories { 67 | maven {//添加repo本地仓库 68 | url uri("repo") 69 | } 70 | google() 71 | jcenter() 72 | } 73 | dependencies { 74 | classpath 'com.android.tools.build:gradle:3.5.3' 75 | //添加插件依赖 76 | classpath 'com.tzx.ams:ams-plugin:1.0.0' 77 | } 78 | } 79 | ``` 80 | 81 | > 项目的 `build.gradle` 配置 82 | 83 | ```groovy 84 | apply plugin: 'com.android.application' 85 | apply plugin: 'amsplugin' 86 | /***部分代码省略***/ 87 | ``` 88 | 89 | ## plugin自定义配置 90 | 91 | > 配置类 92 | 93 | ```java 94 | public class AmsConfig { 95 | //日志开关 96 | public boolean isDebug; 97 | //class包含str则不处理 98 | public String[] filterContainsClassStr; 99 | //class以str开头则不处理 100 | public String[] filterstartsWithClassStr; 101 | //拦截在这个文件中声明的class 102 | public String filterClassFile; 103 | public List filterClassNameList; 104 | //需要进行注入的method 105 | public String amsMethodFile; 106 | //需要进行注入的method对应的tag 107 | public String amsMethodTag; 108 | public List> amsMethodFileList; 109 | } 110 | ``` 111 | 112 | > 创建`AmsConfig` 的类对象 113 | 114 | ```java 115 | public class AmsPlugin implements Plugin { 116 | @Override 117 | public void apply(@NonNull Project project) { 118 | AppExtension appExtension = project.getExtensions().findByType(AppExtension.class); 119 | assert appExtension != null; 120 | //注册优先于task任务的添加 121 | project.getExtensions().create("AmsConfig", AmsConfig.class); 122 | appExtension.registerTransform(new AmsTransform(project)); 123 | } 124 | } 125 | ``` 126 | 127 | > `build.gradle` 文件中定义配置(如果gradle中没有定义,那么会反射构造一个对象) 128 | 129 | ```groovy 130 | apply plugin: 'com.android.application' 131 | apply plugin: 'amsplugin' 132 | AmsConfig{ 133 | isDebug = true 134 | filterContainsClassStr = ["R.class", ".R\$"] 135 | filterstartsWithClassStr = ["android"] 136 | filterClassFile = "amsfilterclass.text"//build.gradle相同目录级别的文件名 137 | amsMethodFile = "amsmethods.text"//build.gradle相同目录级别的文件名 138 | amsMethodTag = "TEST" 139 | } 140 | ``` 141 | 142 | > 获取`gradle` 中定义的配置 143 | 144 | ```java 145 | AmsConfig amsConfig = (AmsConfig) this.project.getExtensions().getByName(AmsConfig.class.getSimpleName()); 146 | //配置文件的获取 147 | String projectDri = this.project.getProjectDir().getAbsolutePath() 148 | String fileName = projectDri + File.separatorChar + amsConfig.filterClassFile; 149 | ``` 150 | 151 | 这个 `amsConfig` 在使用的时候不用判空,如果调用`project.getExtensions().create` 添加了那么就会反射构造出这个对象。如果`gradle` 文件中定义了,那么这个对象就会被赋与相应的属性值。 152 | 153 | 如果在获取的时候,没有被`create`那么就会跑出一下异常: 154 | 155 | ```log 156 | * Where: 157 | Build file '/Users/tanzx/AndroidStudioWorkSpace/GitHub/AmsDemo/app/build.gradle' line: 3 158 | * What went wrong: 159 | A problem occurred evaluating project ':app'. 160 | > Could not find method AmsConfig() for arguments [build_5n6idkxwtmzfflm5k30ynjblo$_run_closure1@2cf1f355] on project ':app' of type org.gradle.api.Project 161 | ``` 162 | 163 | ## 日志 164 | 165 | `./gradlew build` 的日志 166 | 167 | ```shell 168 | > Task :app:transformClassesWithAmsTransformForRelease 169 | AmsTransform filterClassName:com.tzx.amsdemo.App.class 170 | AmsTransform filterClassName:com.tzx.amsdemo.BuildConfig.class 171 | AmsTransform amsMethodFile:maintest#com.tzx.amsdemo.MainActivity.class 172 | AmsTransform Find jar input: android.local.jars:AmsTestJar_V1.0.jar:2069720caf021a802230197880600b9eb8ea02c8 173 | AmsTransform Modifyjar: com/tzx/ams_test_jar/AmsTestJar.class 174 | AmsMethodVisitor visitAnnotation Lcom/tzx/ams/TrackMethod; 175 | AmsMethodVisitor TEST methodName=test 176 | AmsTransform Modifyjar: com/tzx/ams_test_jar/BuildConfig.class 177 | AmsTransform Find jar input: androidx.appcompat:appcompat:1.1.0 178 | AmsTransform Find jar input: androidx.constraintlayout:constraintlayout:1.1.3 179 | AmsTransform Find jar input: androidx.fragment:fragment:1.1.0 180 | AmsTransform Find jar input: androidx.appcompat:appcompat-resources:1.1.0 181 | AmsTransform Find jar input: androidx.drawerlayout:drawerlayout:1.0.0 182 | AmsTransform Find jar input: androidx.viewpager:viewpager:1.0.0 183 | AmsTransform Find jar input: androidx.loader:loader:1.0.0 184 | AmsTransform Find jar input: androidx.activity:activity:1.0.0 185 | AmsTransform Find jar input: androidx.vectordrawable:vectordrawable-animated:1.1.0 186 | AmsTransform Find jar input: androidx.vectordrawable:vectordrawable:1.1.0 187 | AmsTransform Find jar input: androidx.customview:customview:1.0.0 188 | AmsTransform Find jar input: androidx.core:core:1.1.0 189 | AmsTransform Find jar input: androidx.cursoradapter:cursoradapter:1.0.0 190 | AmsTransform Find jar input: androidx.versionedparcelable:versionedparcelable:1.1.0 191 | AmsTransform Find jar input: androidx.collection:collection:1.1.0 192 | AmsTransform Find jar input: androidx.lifecycle:lifecycle-runtime:2.1.0 193 | AmsTransform Find jar input: androidx.lifecycle:lifecycle-viewmodel:2.1.0 194 | AmsTransform Find jar input: androidx.savedstate:savedstate:1.0.0 195 | AmsTransform Find jar input: androidx.interpolator:interpolator:1.0.0 196 | AmsTransform Find jar input: androidx.lifecycle:lifecycle-livedata:2.0.0 197 | AmsTransform Find jar input: androidx.lifecycle:lifecycle-livedata-core:2.0.0 198 | AmsTransform Find jar input: androidx.arch.core:core-runtime:2.0.0 199 | AmsTransform Find jar input: androidx.arch.core:core-common:2.1.0 200 | AmsTransform Find jar input: androidx.lifecycle:lifecycle-common:2.1.0 201 | AmsTransform Find jar input: androidx.annotation:annotation:1.1.0 202 | AmsTransform Find jar input: androidx.constraintlayout:constraintlayout-solver:1.1.3 203 | AmsTransform Find jar input: :ams 204 | AmsTransform Modifyjar: com/tzx/ams/BuildConfig.class 205 | AmsTransform Modifyjar: com/tzx/ams/MethodEventManager.class 206 | AmsTransform Modifyjar: com/tzx/ams/MethodObserver.class 207 | AmsTransform Modifyjar: com/tzx/ams/TrackMethod.class 208 | AmsTransform Find dir input:classes 209 | AmsTransform Modifydir: com.tzx.amsdemo.MainActivity.class 210 | AmsClassVisitor visitMethod: com/tzx/amsdemo/MainActivity TEST methodName= maintest 211 | AmsTransform Modifydir: com.tzx.amsdemo.TimeObserver.class 212 | ``` 213 | 214 | -------------------------------------------------------------------------------- /ams-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /ams-plugin/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'groovy' 2 | apply plugin: 'maven' 3 | 4 | dependencies { 5 | implementation gradleApi() 6 | implementation localGroovy() 7 | implementation 'com.android.tools.build:gradle:3.4.1' 8 | } 9 | 10 | repositories { 11 | mavenCentral() 12 | jcenter() 13 | google() 14 | } 15 | 16 | uploadArchives { 17 | repositories.mavenDeployer { 18 | repository(url: uri('../repo')) 19 | pom.groupId = 'com.tzx.ams' 20 | pom.artifactId = 'ams-plugin' 21 | pom.version = '1.0.0' 22 | } 23 | } -------------------------------------------------------------------------------- /ams-plugin/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stven0king/AndroidPluginStudy/be06d9f7381e0ee4657903cfc9ff4ddfe0b1a9fd/ams-plugin/consumer-rules.pro -------------------------------------------------------------------------------- /ams-plugin/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /ams-plugin/src/main/java/com/tzx/ams/plugin/AmsClassVisitor.java: -------------------------------------------------------------------------------- 1 | package com.tzx.ams.plugin; 2 | 3 | import com.android.utils.Pair; 4 | 5 | import org.objectweb.asm.ClassVisitor; 6 | import org.objectweb.asm.MethodVisitor; 7 | import org.objectweb.asm.Opcodes; 8 | 9 | import java.io.File; 10 | 11 | /** 12 | * Created by Tanzhenxing 13 | * Date: 2020-01-15 16:44 14 | * Description: 15 | */ 16 | public class AmsClassVisitor extends ClassVisitor { 17 | private static final String TAG = "AmsClassVisitor"; 18 | private String className; 19 | public AmsClassVisitor(ClassVisitor classVisitor) { 20 | super(Opcodes.ASM6 ,classVisitor); 21 | } 22 | 23 | /** 24 | * 访问类的头部 25 | * @param version version指的是类的版本 26 | * @param access 指的是类的修饰符 27 | * @param name 类的名称 28 | * @param signature 类的签名,如果类不是泛型或者没有继承泛型类,那么signature为空 29 | * @param superName 类的父类名称 30 | * @param interfaces 类继承的接口 31 | */ 32 | @Override 33 | public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { 34 | super.visit(version, access, name, signature, superName, interfaces); 35 | this.className = name; 36 | } 37 | 38 | /** 39 | * 访问类的方法,如果需要修改类方法信息,则可以重写此方法; 40 | * @param access 表示该域的访问方式,public,private或者static,final等等; 41 | * @param name 指的是方法的名称; 42 | * @param desc 表示方法的参数类型和返回值类型; 43 | * @param signature 指的是域的签名,一般是泛型域才会有签名; 44 | * @param exceptions 45 | * @return 46 | */ 47 | @Override 48 | public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { 49 | MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions); 50 | AmsMethodVisitor amsMethodVisitor = new AmsMethodVisitor(methodVisitor, access, name, desc); 51 | AmsConfig amsConfig = AmsTransform.getAmsConfig(); 52 | if (amsConfig.amsMethodFileList != null && amsConfig.amsMethodFileList.size() > 0 53 | && amsConfig.amsMethodTag != null && amsConfig.amsMethodTag.length() > 0) { 54 | for (Pair p: amsConfig.amsMethodFileList) { 55 | if (p.getFirst() != null && p.getSecond() != null) { 56 | String cName = p.getSecond().replace(".class", "").replace('.', File.separatorChar); 57 | if (name.equals(p.getFirst()) && cName.equals(this.className)) { 58 | amsMethodVisitor.setTag(amsConfig.amsMethodTag); 59 | Logger.logs(TAG, "visitMethod:", this.className, amsConfig.amsMethodTag, "methodName=", name); 60 | break; 61 | } 62 | } 63 | } 64 | } 65 | return amsMethodVisitor; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ams-plugin/src/main/java/com/tzx/ams/plugin/AmsConfig.java: -------------------------------------------------------------------------------- 1 | package com.tzx.ams.plugin; 2 | 3 | import com.android.utils.Pair; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Created by Tanzhenxing 9 | * Date: 2020-03-02 22:14 10 | * Description: 11 | */ 12 | public class AmsConfig { 13 | //日志开关 14 | public boolean isDebug; 15 | //class包含str则不处理 16 | public String[] filterContainsClassStr; 17 | //class以str开头则不处理 18 | public String[] filterstartsWithClassStr; 19 | //拦截在这个文件中声明的class 20 | public String filterClassFile; 21 | public List filterClassNameList; 22 | //需要进行注入的method 23 | public String amsMethodFile; 24 | //需要进行注入的method对应的tag 25 | public String amsMethodTag; 26 | public List> amsMethodFileList; 27 | } 28 | -------------------------------------------------------------------------------- /ams-plugin/src/main/java/com/tzx/ams/plugin/AmsMethodVisitor.java: -------------------------------------------------------------------------------- 1 | package com.tzx.ams.plugin; 2 | 3 | import com.android.utils.Pair; 4 | 5 | import org.objectweb.asm.AnnotationVisitor; 6 | import org.objectweb.asm.MethodVisitor; 7 | import org.objectweb.asm.Opcodes; 8 | import org.objectweb.asm.commons.AdviceAdapter; 9 | 10 | import java.io.File; 11 | 12 | /** 13 | * Created by Tanzhenxing 14 | * Date: 2020-01-15 16:53 15 | * Description: 方法解析 16 | */ 17 | public class AmsMethodVisitor extends AdviceAdapter { 18 | private static final String TAG = "AmsMethodVisitor"; 19 | private static final String ANNOTATION_TRACK_METHOD = "Lcom/tzx/ams/TrackMethod;"; 20 | private static final String METHOD_EVENT_MANAGER = "com/tzx/ams/MethodEventManager"; 21 | private final MethodVisitor methodVisitor; 22 | private final String methodName; 23 | 24 | private boolean needInject; 25 | private String tag; 26 | 27 | public AmsMethodVisitor(MethodVisitor methodVisitor, int access, String name, String desc) { 28 | super(Opcodes.ASM6, methodVisitor, access, name, desc); 29 | this.methodVisitor = methodVisitor; 30 | this.methodName = name; 31 | } 32 | 33 | public void setTag(String tag) { 34 | needInject = true; 35 | this.tag = tag; 36 | } 37 | 38 | /** 39 | * 访问类的注解 40 | * @param desc 表示类注解类的描述; 41 | * @param visible 表示该注解是否运行时可见 42 | * @return 43 | */ 44 | @Override 45 | public AnnotationVisitor visitAnnotation(String desc, boolean visible) { 46 | Logger.logs(TAG, "visitAnnotation", desc); 47 | AnnotationVisitor annotationVisitor = super.visitAnnotation(desc, visible); 48 | if (desc.equals(ANNOTATION_TRACK_METHOD)) { 49 | needInject = true; 50 | return new AnnotationVisitor(Opcodes.ASM6, annotationVisitor) { 51 | /** 52 | * @param name 注解key值 53 | * @param value value值 54 | */ 55 | @Override 56 | public void visit(String name, Object value) { 57 | super.visit(name, value); 58 | if (name.equals("tag") && value instanceof String) { 59 | tag = (String) value; 60 | Logger.log(TAG, tag, " methodName=" + methodName); 61 | } 62 | } 63 | }; 64 | } 65 | return annotationVisitor; 66 | } 67 | 68 | /** 69 | * 访问方法操作指令 70 | * @param opcode 为INVOKESPECIAL,INVOKESTATIC,INVOKEVIRTUAL,INVOKEINTERFACE; 71 | * @param owner 方法拥有者的名称; 72 | * @param name 方法名称; 73 | * @param desc 方法描述,参数和返回值; 74 | * @param itf 是否是接口; 75 | */ 76 | @Override 77 | public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { 78 | super.visitMethodInsn(opcode, owner, name, desc, itf); 79 | } 80 | 81 | @Override 82 | protected void onMethodEnter() { 83 | super.onMethodEnter(); 84 | handleMethodEnter(); 85 | } 86 | 87 | @Override 88 | protected void onMethodExit(int opcode) { 89 | super.onMethodExit(opcode); 90 | handleMethodExit(); 91 | } 92 | 93 | private void handleMethodEnter() { 94 | if (needInject && tag != null) { 95 | //visitMethodInsn:访问方法操作指令 96 | //opcode:为INVOKESPECIAL,INVOKESTATIC,INVOKEVIRTUAL,INVOKEINTERFACE; 97 | //owner:方法拥有者的名称; 98 | //name:方法名称; 99 | //descriptor:方法描述,参数和返回值; 100 | //isInterface;是否是接口; 101 | methodVisitor.visitMethodInsn(INVOKESTATIC, METHOD_EVENT_MANAGER, 102 | "getInstance", "()L"+METHOD_EVENT_MANAGER+";", false); 103 | //visitLdcInsn:访问ldc指令,也就是访问常量池索引; 104 | //value:必须是非空的Integer,Float,Double,Long,String,或者对象的Type,Array的Type,Method Sort的Type,或者Method Handle常量中的Handle,或者ConstantDynamic; 105 | methodVisitor.visitLdcInsn(tag); 106 | methodVisitor.visitLdcInsn(methodName); 107 | methodVisitor.visitMethodInsn(INVOKEVIRTUAL, METHOD_EVENT_MANAGER, 108 | "notifyMethodEnter", "(Ljava/lang/String;Ljava/lang/String;)V", false); 109 | } 110 | } 111 | 112 | private void handleMethodExit() { 113 | if (needInject && tag != null) { 114 | methodVisitor.visitMethodInsn(INVOKESTATIC, METHOD_EVENT_MANAGER, 115 | "getInstance", "()L"+METHOD_EVENT_MANAGER+";", false); 116 | methodVisitor.visitLdcInsn(tag); 117 | methodVisitor.visitLdcInsn(methodName); 118 | methodVisitor.visitMethodInsn(INVOKEVIRTUAL, METHOD_EVENT_MANAGER, 119 | "notifyMethodExit", "(Ljava/lang/String;Ljava/lang/String;)V", false); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ams-plugin/src/main/java/com/tzx/ams/plugin/AmsPlugin.java: -------------------------------------------------------------------------------- 1 | package com.tzx.ams.plugin; 2 | 3 | 4 | import com.android.annotations.NonNull; 5 | import com.android.build.gradle.AppExtension; 6 | 7 | import org.gradle.api.Plugin; 8 | import org.gradle.api.Project; 9 | 10 | /** 11 | * Created by Tanzhenxing 12 | * Date: 2020-01-15 16:40 13 | * Description: 14 | */ 15 | public class AmsPlugin implements Plugin { 16 | @Override 17 | public void apply(@NonNull Project project) { 18 | AppExtension appExtension = project.getExtensions().findByType(AppExtension.class); 19 | assert appExtension != null; 20 | //注册优先于task任务的添加 21 | project.getExtensions().create("AmsConfig", AmsConfig.class); 22 | appExtension.registerTransform(new AmsTransform(project)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ams-plugin/src/main/java/com/tzx/ams/plugin/AmsTransform.java: -------------------------------------------------------------------------------- 1 | package com.tzx.ams.plugin; 2 | 3 | import com.android.build.api.transform.DirectoryInput; 4 | import com.android.build.api.transform.Format; 5 | import com.android.build.api.transform.JarInput; 6 | import com.android.build.api.transform.QualifiedContent; 7 | import com.android.build.api.transform.Transform; 8 | import com.android.build.api.transform.TransformException; 9 | import com.android.build.api.transform.TransformInput; 10 | import com.android.build.api.transform.TransformInvocation; 11 | import com.android.build.gradle.internal.pipeline.TransformManager; 12 | import com.android.utils.FileUtils; 13 | import com.android.utils.Pair; 14 | 15 | import org.apache.commons.codec.digest.DigestUtils; 16 | import org.apache.commons.compress.utils.IOUtils; 17 | import org.gradle.api.Project; 18 | import org.objectweb.asm.ClassReader; 19 | import org.objectweb.asm.ClassVisitor; 20 | import org.objectweb.asm.ClassWriter; 21 | 22 | import java.io.BufferedReader; 23 | import java.io.File; 24 | import java.io.FileInputStream; 25 | import java.io.FileNotFoundException; 26 | import java.io.FileOutputStream; 27 | import java.io.FileReader; 28 | import java.io.IOException; 29 | import java.io.InputStream; 30 | import java.util.ArrayList; 31 | import java.util.Collection; 32 | import java.util.Enumeration; 33 | import java.util.HashMap; 34 | import java.util.Map; 35 | import java.util.Objects; 36 | import java.util.Set; 37 | import java.util.jar.JarEntry; 38 | import java.util.jar.JarFile; 39 | import java.util.jar.JarOutputStream; 40 | 41 | /** 42 | * Created by Tanzhenxing 43 | * Date: 2020-01-15 16:59 44 | * Description: 自定义Transform 45 | */ 46 | public class AmsTransform extends Transform{ 47 | private static final String TAG = "AmsTransform"; 48 | private Map modifyMap = new HashMap<>(); 49 | private Project project; 50 | private static AmsConfig amsConfig; 51 | public AmsTransform(Project project) { 52 | this.project = project; 53 | } 54 | 55 | @Override 56 | public String getName() { 57 | return AmsTransform.class.getSimpleName(); 58 | } 59 | 60 | @Override 61 | public Set getInputTypes() { 62 | return TransformManager.CONTENT_CLASS; 63 | } 64 | 65 | @Override 66 | public Set getScopes() { 67 | return TransformManager.SCOPE_FULL_PROJECT; 68 | } 69 | 70 | @Override 71 | public boolean isIncremental() { 72 | return false;//是否开启增量编译 73 | } 74 | 75 | public static AmsConfig getAmsConfig() { 76 | return amsConfig; 77 | } 78 | 79 | private void initConfig() { 80 | amsConfig = (AmsConfig) this.project.getExtensions().getByName(AmsConfig.class.getSimpleName()); 81 | Logger.isDebug = amsConfig.isDebug; 82 | String projectDri = this.project.getProjectDir().getAbsolutePath(); 83 | initfilterClassFile(projectDri); 84 | initAmsMethodFile(projectDri); 85 | } 86 | 87 | private void initfilterClassFile(String projectDri) { 88 | if (amsConfig.filterClassFile != null && amsConfig.filterClassFile.length() != 0) { 89 | String fileName = projectDri + File.separatorChar + amsConfig.filterClassFile; 90 | try { 91 | FileReader fileReader = new FileReader(fileName); 92 | BufferedReader bufferedReader = new BufferedReader(fileReader); 93 | String line; 94 | while ((line = bufferedReader.readLine()) != null) { 95 | Logger.log(TAG, "filterClassName:" + line); 96 | if (amsConfig.filterClassNameList == null) { 97 | amsConfig.filterClassNameList = new ArrayList<>(); 98 | } 99 | if (line.length() > 0) { 100 | amsConfig.filterClassNameList.add(line); 101 | } 102 | } 103 | bufferedReader.close(); 104 | fileReader.close(); 105 | } catch (FileNotFoundException e) { 106 | e.printStackTrace(); 107 | } catch (IOException e) { 108 | e.printStackTrace(); 109 | } 110 | } 111 | } 112 | 113 | private void initAmsMethodFile(String projectDri) { 114 | if (amsConfig.amsMethodFile != null && amsConfig.amsMethodFile.length() > 0) { 115 | String fileName = projectDri + File.separatorChar + amsConfig.amsMethodFile; 116 | try { 117 | FileReader fileReader = new FileReader(fileName); 118 | BufferedReader bufferedReader = new BufferedReader(fileReader); 119 | String line; 120 | while ((line = bufferedReader.readLine()) != null) { 121 | Logger.log(TAG, "amsMethodFile:" + line); 122 | if (amsConfig.amsMethodFileList == null) { 123 | amsConfig.amsMethodFileList = new ArrayList<>(); 124 | } 125 | if (line.length() > 0) { 126 | String[] strings = line.split("#"); 127 | if (strings.length != 2) { 128 | continue; 129 | } 130 | String method = strings[0]; 131 | String classname = strings[1]; 132 | amsConfig.amsMethodFileList.add(Pair.of(method, classname)); 133 | } 134 | } 135 | bufferedReader.close(); 136 | fileReader.close(); 137 | } catch (FileNotFoundException e) { 138 | e.printStackTrace(); 139 | } catch (IOException e) { 140 | e.printStackTrace(); 141 | } 142 | } 143 | } 144 | 145 | @Override 146 | public void transform(TransformInvocation transformInvocation) 147 | throws TransformException, InterruptedException, IOException { 148 | super.transform(transformInvocation); 149 | //transform方法中才能获取到注册的对象 150 | initConfig(); 151 | if (!isIncremental()) { 152 | transformInvocation.getOutputProvider().deleteAll(); 153 | } 154 | // 获取输入(消费型输入,需要传递给下一个Transform) 155 | Collection inputs = transformInvocation.getInputs(); 156 | for (TransformInput input : inputs) { 157 | // 遍历输入,分别遍历其中的jar以及directory 158 | for (JarInput jarInput : input.getJarInputs()) { 159 | //对jar文件进行处理 160 | Logger.log(TAG, "Find jar input: " + jarInput.getName()); 161 | transformJar(transformInvocation, jarInput); 162 | } 163 | for (DirectoryInput directoryInput : input.getDirectoryInputs()) { 164 | // 对directory进行处理 165 | Logger.log(TAG, "Find dir input:" + directoryInput.getFile().getName()); 166 | transformDirectory(transformInvocation, directoryInput); 167 | } 168 | } 169 | } 170 | 171 | 172 | private void transformJar(TransformInvocation invocation, JarInput input) throws IOException { 173 | File tempDir = invocation.getContext().getTemporaryDir(); 174 | String destName = input.getFile().getName(); 175 | String hexName = DigestUtils.md5Hex(input.getFile().getAbsolutePath()).substring(0, 8); 176 | if (destName.endsWith(".jar")) { 177 | destName = destName.substring(0, destName.length() - 4); 178 | } 179 | // 获取输出路径 180 | File dest = invocation.getOutputProvider() 181 | //.getContentLocation(input.getFile().getAbsolutePath(), input.getContentTypes(), input.getScopes(), Format.JAR); 182 | .getContentLocation(destName + "_" + hexName, input.getContentTypes(), input.getScopes(), Format.JAR); 183 | JarFile originJar = new JarFile(input.getFile()); 184 | //input:/build/intermediates/runtime_library_classes/release/classes.jar 185 | File outputJar = new File(tempDir, "temp_"+input.getFile().getName()); 186 | //out:/build/tmp/transformClassesWithAmsTransformForRelease/temp_classes.jar 187 | //dest:/build/intermediates/transforms/AmsTransform/release/26.jar 188 | JarOutputStream output = new JarOutputStream(new FileOutputStream(outputJar)); 189 | 190 | // 遍历原jar文件寻找class文件 191 | Enumeration enumeration = originJar.entries(); 192 | while (enumeration.hasMoreElements()) { 193 | JarEntry originEntry = enumeration.nextElement(); 194 | InputStream inputStream = originJar.getInputStream(originEntry); 195 | String entryName = originEntry.getName(); 196 | if (entryName.endsWith(".class")) { 197 | JarEntry destEntry = new JarEntry(entryName); 198 | output.putNextEntry(destEntry); 199 | byte[] sourceBytes = IOUtils.toByteArray(inputStream); 200 | // 修改class文件内容 201 | byte[] modifiedBytes = null; 202 | if (filterModifyClass(entryName)) { 203 | Logger.log(TAG, "Modifyjar:" , entryName); 204 | modifiedBytes = modifyClass(sourceBytes); 205 | } 206 | if (modifiedBytes == null) { 207 | modifiedBytes = sourceBytes; 208 | } 209 | output.write(modifiedBytes); 210 | } 211 | output.closeEntry(); 212 | } 213 | output.close(); 214 | originJar.close(); 215 | // 复制修改后jar到输出路径 216 | FileUtils.copyFile(outputJar, dest); 217 | } 218 | 219 | 220 | private void transformDirectory(TransformInvocation invocation, DirectoryInput input) throws IOException { 221 | File tempDir = invocation.getContext().getTemporaryDir(); 222 | // 获取输出路径 223 | File dest = invocation.getOutputProvider() 224 | .getContentLocation(input.getName(), input.getContentTypes(), input.getScopes(), Format.DIRECTORY); 225 | File dir = input.getFile(); 226 | if (dir != null && dir.exists()) { 227 | //tempDir=build/tmp/transformClassesWithAmsTransformForDebug 228 | //dir=build/intermediates/javac/debug/compileDebugJavaWithJavac/classes 229 | 230 | traverseDirectory(dir.getAbsolutePath(), tempDir, dir); 231 | //Map modifiedMap = new HashMap<>(); 232 | //traverseDirectory(tempDir, dir, modifiedMap, dir.getAbsolutePath() + File.separator); 233 | 234 | //input.getFile=build/intermediates/javac/debug/compileDebugJavaWithJavac/classes 235 | //dest=build/intermediates/transforms/AmsTransform/debug/52 236 | 237 | FileUtils.copyDirectory(input.getFile(), dest); 238 | 239 | for (Map.Entry entry : modifyMap.entrySet()) { 240 | File target = new File(dest.getAbsolutePath() + File.separatorChar + entry.getKey().replace('.', File.separatorChar) + ".class"); 241 | if (target.exists()) { 242 | target.delete(); 243 | } 244 | FileUtils.copyFile(entry.getValue(), target); 245 | entry.getValue().delete(); 246 | } 247 | } 248 | } 249 | 250 | /** 251 | * 遍历目录下面的class文件 252 | * @param basedir 基准目录,和dir对比需要找到包路径 253 | * @param tempDir 需要写入的临时目录 254 | * @param dir class文件目录 255 | * @throws IOException 256 | */ 257 | private void traverseDirectory(String basedir, File tempDir, File dir) throws IOException { 258 | for (File file : Objects.requireNonNull(dir.listFiles())) { 259 | if (file.isDirectory()) { 260 | traverseDirectory(basedir, tempDir, file); 261 | } else if (file.getAbsolutePath().endsWith(".class")) { 262 | String className = path2ClassName(file.getAbsolutePath() 263 | .replace(basedir + File.separator, "")); 264 | byte[] sourceBytes = IOUtils.toByteArray(new FileInputStream(file)); 265 | byte[] modifiedBytes = null; 266 | if (filterModifyClass(className + ".class")) { 267 | Logger.log(TAG, "Modifydir:" , className + ".class"); 268 | modifiedBytes = modifyClass(sourceBytes); 269 | } 270 | if (modifiedBytes == null) { 271 | modifiedBytes = sourceBytes; 272 | } 273 | File modified = new File(tempDir, className + ".class"); 274 | if (modified.exists()) { 275 | modified.delete(); 276 | } 277 | modified.createNewFile(); 278 | new FileOutputStream(modified).write(modifiedBytes); 279 | modifyMap.put(className, modified); 280 | } 281 | } 282 | } 283 | 284 | private boolean filterModifyClass(String className) { 285 | if (className == null || className.length() == 0) return false; 286 | String s = className.replace(File.separator, "."); 287 | if (amsConfig.filterClassNameList != null && amsConfig.filterClassNameList.size() > 0) { 288 | for (String str: amsConfig.filterClassNameList) { 289 | if (s.equals(str)) { 290 | return false; 291 | } 292 | } 293 | } 294 | if (amsConfig.filterContainsClassStr != null && amsConfig.filterContainsClassStr.length > 0) { 295 | for (String str: amsConfig.filterContainsClassStr) { 296 | if (s.contains(str)) { 297 | return false; 298 | } 299 | } 300 | } 301 | if (amsConfig.filterstartsWithClassStr != null && amsConfig.filterstartsWithClassStr.length > 0) { 302 | for (String str: amsConfig.filterstartsWithClassStr) { 303 | if (s.startsWith(str)) { 304 | return false; 305 | } 306 | } 307 | } 308 | return true; 309 | } 310 | 311 | private byte[] modifyClass(byte[] classBytes) { 312 | ClassReader classReader = new ClassReader(classBytes); 313 | ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); 314 | ClassVisitor classVisitor = new AmsClassVisitor(classWriter); 315 | classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES); 316 | return classWriter.toByteArray(); 317 | } 318 | 319 | static String path2ClassName(String pathName) { 320 | return pathName.replace(File.separator, ".").replace(".class", ""); 321 | } 322 | 323 | } 324 | -------------------------------------------------------------------------------- /ams-plugin/src/main/java/com/tzx/ams/plugin/Logger.java: -------------------------------------------------------------------------------- 1 | package com.tzx.ams.plugin; 2 | 3 | /** 4 | * Created by Tanzhenxing 5 | * Date: 2020-03-03 10:07 6 | * Description: 7 | */ 8 | public class Logger { 9 | public static boolean isDebug = false; 10 | public static void log(String tag) { 11 | if (isDebug) { 12 | System.out.println(tag); 13 | } 14 | } 15 | public static void log(String tag, String s) { 16 | logs(tag, s); 17 | } 18 | 19 | public static void log(String tag, String s, String s2) { 20 | logs(tag, s, s2); 21 | } 22 | 23 | public static void logs(String tag, String... s) { 24 | if (!isDebug) return; 25 | StringBuilder stringBuilder = new StringBuilder(tag); 26 | if (s != null && s.length > 0) { 27 | for (String ss:s) { 28 | stringBuilder.append(" " + ss); 29 | } 30 | } 31 | System.out.println(stringBuilder.toString()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ams-plugin/src/main/resources/META-INF/gradle-plugins/amsplugin.properties: -------------------------------------------------------------------------------- 1 | implementation-class=com.tzx.ams.plugin.AmsPlugin -------------------------------------------------------------------------------- /ams/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /ams/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 28 5 | 6 | 7 | defaultConfig { 8 | minSdkVersion 19 9 | targetSdkVersion 28 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | compileOptions { 21 | sourceCompatibility = '1.8' 22 | targetCompatibility = '1.8' 23 | } 24 | 25 | } 26 | 27 | dependencies { 28 | implementation fileTree(dir: 'libs', include: ['*.jar']) 29 | } 30 | -------------------------------------------------------------------------------- /ams/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stven0king/AndroidPluginStudy/be06d9f7381e0ee4657903cfc9ff4ddfe0b1a9fd/ams/consumer-rules.pro -------------------------------------------------------------------------------- /ams/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /ams/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /ams/src/main/java/com/tzx/ams/MethodEventManager.java: -------------------------------------------------------------------------------- 1 | package com.tzx.ams; 2 | 3 | 4 | 5 | import java.util.ArrayList; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | /** 11 | * Created by Tanzhenxing 12 | * Date: 2020-01-15 16:31 13 | * Description: 14 | */ 15 | public class MethodEventManager { 16 | 17 | private static volatile MethodEventManager INSTANCE; 18 | private Map> mObserverMap = new HashMap<>(); 19 | 20 | private MethodEventManager() { 21 | } 22 | 23 | public static MethodEventManager getInstance() { 24 | if (INSTANCE == null) { 25 | synchronized (MethodEventManager.class) { 26 | if (INSTANCE == null) { 27 | INSTANCE = new MethodEventManager(); 28 | } 29 | } 30 | } 31 | return INSTANCE; 32 | } 33 | 34 | public void registerMethodObserver(String tag, MethodObserver listener) { 35 | if (listener == null) { 36 | return; 37 | } 38 | 39 | List listeners = mObserverMap.get(tag); 40 | if (listeners == null) { 41 | listeners = new ArrayList<>(); 42 | } 43 | listeners.add(listener); 44 | mObserverMap.put(tag, listeners); 45 | } 46 | 47 | public void notifyMethodEnter(String tag, String methodName) { 48 | List listeners = mObserverMap.get(tag); 49 | if (listeners == null) { 50 | return; 51 | } 52 | for (MethodObserver listener : listeners) { 53 | listener.onMethodEnter(tag, methodName); 54 | } 55 | } 56 | 57 | public void notifyMethodExit(String tag, String methodName) { 58 | List listeners = mObserverMap.get(tag); 59 | if (listeners == null) { 60 | return; 61 | } 62 | for (MethodObserver listener : listeners) { 63 | listener.onMethodExit(tag, methodName); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ams/src/main/java/com/tzx/ams/MethodObserver.java: -------------------------------------------------------------------------------- 1 | package com.tzx.ams; 2 | 3 | /** 4 | * Created by Tanzhenxing 5 | * Date: 2020-01-15 16:34 6 | * Description: 7 | */ 8 | public interface MethodObserver { 9 | default void onMethodEnter(String tag, String methodName) {} 10 | default void onMethodExit(String tag, String methodName) {} 11 | } 12 | -------------------------------------------------------------------------------- /ams/src/main/java/com/tzx/ams/TrackMethod.java: -------------------------------------------------------------------------------- 1 | package com.tzx.ams; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Created by Tanzhenxing 10 | * Date: 2020-01-14 10:17 11 | * Description: 12 | */ 13 | @Target(ElementType.METHOD) 14 | @Retention(RetentionPolicy.CLASS) 15 | public @interface TrackMethod { 16 | String tag(); 17 | } 18 | 19 | -------------------------------------------------------------------------------- /ams/src/main/res/drawable/ams_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stven0king/AndroidPluginStudy/be06d9f7381e0ee4657903cfc9ff4ddfe0b1a9fd/ams/src/main/res/drawable/ams_test.png -------------------------------------------------------------------------------- /ams/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ams 3 | 4 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/amsfilterclass.text: -------------------------------------------------------------------------------- 1 | com.tzx.amsdemo.App.class 2 | com.tzx.amsdemo.BuildConfig.class -------------------------------------------------------------------------------- /app/amsmethods.text: -------------------------------------------------------------------------------- 1 | maintest#com.tzx.amsdemo.MainActivity.class -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'amsplugin' 3 | AmsConfig{ 4 | isDebug = true 5 | filterContainsClassStr = ["R.class", ".R\$"] 6 | filterstartsWithClassStr = ["android"] 7 | filterClassFile = "amsfilterclass.text" 8 | amsMethodFile = "amsmethods.text" 9 | amsMethodTag = "TEST" 10 | } 11 | android { 12 | compileSdkVersion 29 13 | buildToolsVersion "29.0.2" 14 | defaultConfig { 15 | applicationId "com.tzx.amsdemo" 16 | minSdkVersion 21 17 | targetSdkVersion 29 18 | versionCode 1 19 | versionName "1.0" 20 | } 21 | buildTypes { 22 | release { 23 | minifyEnabled false 24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 25 | } 26 | } 27 | 28 | compileOptions { 29 | sourceCompatibility = '1.8' 30 | targetCompatibility = '1.8' 31 | } 32 | } 33 | dependencies { 34 | implementation fileTree(include: ['*.jar'], dir: 'libs') 35 | implementation 'androidx.appcompat:appcompat:1.1.0' 36 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 37 | implementation project(':ams') 38 | implementation files('libs/AmsTestJar_V1.0.jar') 39 | } 40 | 41 | -------------------------------------------------------------------------------- /app/libs/AmsTestJar_V1.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stven0king/AndroidPluginStudy/be06d9f7381e0ee4657903cfc9ff4ddfe0b1a9fd/app/libs/AmsTestJar_V1.0.jar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/tzx/amsdemo/App.java: -------------------------------------------------------------------------------- 1 | package com.tzx.amsdemo; 2 | 3 | import android.app.Application; 4 | 5 | import com.tzx.ams.MethodEventManager; 6 | 7 | /** 8 | * Created by Tanzhenxing 9 | * Date: 2020-02-27 18:53 10 | * Description: 11 | */ 12 | public class App extends Application { 13 | @Override 14 | public void onCreate() { 15 | super.onCreate(); 16 | MethodEventManager.getInstance().registerMethodObserver("TEST", new TimeObserver()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/tzx/amsdemo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.tzx.amsdemo; 2 | 3 | import androidx.annotation.Nullable; 4 | import androidx.appcompat.app.AppCompatActivity; 5 | 6 | import android.os.Bundle; 7 | import android.util.Log; 8 | 9 | import com.tzx.ams.TrackMethod; 10 | import com.tzx.ams_test_jar.AmsTestJar; 11 | 12 | import java.util.concurrent.ExecutorService; 13 | import java.util.concurrent.Executors; 14 | 15 | public class MainActivity extends AppCompatActivity { 16 | private ExecutorService mExecutor = Executors.newFixedThreadPool(10); 17 | @Override 18 | protected void onCreate(@Nullable Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | for (int i = 0; i < 10; i++) { 21 | mExecutor.execute(this::test); 22 | } 23 | } 24 | 25 | //@TrackMethod(tag = "TIME") 26 | public void test() { 27 | AmsTestJar.test(); 28 | maintest(); 29 | Log.d("tanzhenxing", "test"); 30 | } 31 | 32 | public void maintest() { 33 | Log.d("tanzhenxing", "maintest"); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/tzx/amsdemo/TimeObserver.java: -------------------------------------------------------------------------------- 1 | package com.tzx.amsdemo; 2 | 3 | import android.util.Log; 4 | 5 | import com.tzx.ams.MethodObserver; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /** 11 | * Created by Tanzhenxing 12 | * Date: 2020-01-15 19:51 13 | * Description: 14 | */ 15 | public class TimeObserver implements MethodObserver { 16 | private static final String TAG_METHOD_TIME = "MethodCost"; 17 | private Map enterTimeMap = new HashMap<>(); 18 | 19 | @Override 20 | public void onMethodEnter(String tag, String methodName) { 21 | String key = generateKey(tag, methodName); 22 | Long time = System.currentTimeMillis(); 23 | enterTimeMap.put(key, time); 24 | } 25 | 26 | @Override 27 | public void onMethodExit(String tag, String methodName) { 28 | String key = generateKey(tag, methodName); 29 | Long enterTime = enterTimeMap.get(key); 30 | if (enterTime == null) { 31 | throw new IllegalStateException("method exit without enter"); 32 | } 33 | long cost = System.currentTimeMillis() - enterTime; 34 | Log.d(TAG_METHOD_TIME, "method " + methodName + " cost " 35 | + (double)cost + "ms" + " in thread " + Thread.currentThread().getName()); 36 | enterTimeMap.remove(key); 37 | } 38 | 39 | private String generateKey(String tag, String methodName) { 40 | return tag + methodName + Thread.currentThread().getName(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stven0king/AndroidPluginStudy/be06d9f7381e0ee4657903cfc9ff4ddfe0b1a9fd/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stven0king/AndroidPluginStudy/be06d9f7381e0ee4657903cfc9ff4ddfe0b1a9fd/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stven0king/AndroidPluginStudy/be06d9f7381e0ee4657903cfc9ff4ddfe0b1a9fd/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stven0king/AndroidPluginStudy/be06d9f7381e0ee4657903cfc9ff4ddfe0b1a9fd/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stven0king/AndroidPluginStudy/be06d9f7381e0ee4657903cfc9ff4ddfe0b1a9fd/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stven0king/AndroidPluginStudy/be06d9f7381e0ee4657903cfc9ff4ddfe0b1a9fd/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stven0king/AndroidPluginStudy/be06d9f7381e0ee4657903cfc9ff4ddfe0b1a9fd/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stven0king/AndroidPluginStudy/be06d9f7381e0ee4657903cfc9ff4ddfe0b1a9fd/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stven0king/AndroidPluginStudy/be06d9f7381e0ee4657903cfc9ff4ddfe0b1a9fd/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stven0king/AndroidPluginStudy/be06d9f7381e0ee4657903cfc9ff4ddfe0b1a9fd/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AmsDemo 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | maven { 6 | url uri("repo") 7 | } 8 | google() 9 | jcenter() 10 | 11 | } 12 | dependencies { 13 | classpath 'com.android.tools.build:gradle:3.5.3' 14 | classpath 'com.tzx.ams:ams-plugin:1.0.0' 15 | // NOTE: Do not place your application dependecies here; they belong 16 | // in the individual module build.gradle files 17 | } 18 | } 19 | 20 | allprojects { 21 | repositories { 22 | maven { 23 | url uri("repo") 24 | } 25 | google() 26 | jcenter() 27 | } 28 | } 29 | 30 | task clean(type: Delete) { 31 | delete rootProject.buildDir 32 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | 21 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stven0king/AndroidPluginStudy/be06d9f7381e0ee4657903cfc9ff4ddfe0b1a9fd/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Feb 27 18:43:33 CST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stven0king/AndroidPluginStudy/be06d9f7381e0ee4657903cfc9ff4ddfe0b1a9fd/properties.png -------------------------------------------------------------------------------- /repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stven0king/AndroidPluginStudy/be06d9f7381e0ee4657903cfc9ff4ddfe0b1a9fd/repo.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':ams', ':ams-plugin' 2 | rootProject.name='AmsDemo' 3 | -------------------------------------------------------------------------------- /uploadArchives.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stven0king/AndroidPluginStudy/be06d9f7381e0ee4657903cfc9ff4ddfe0b1a9fd/uploadArchives.png --------------------------------------------------------------------------------