淺談Android ASM自動(dòng)埋點(diǎn)方案實(shí)踐
這段時(shí)間想到一個(gè)有趣的功能,就是在Android的代碼編譯期間進(jìn)行一些騷操作,來(lái)達(dá)到一些日常情境下難以實(shí)現(xiàn)的功能,比如監(jiān)聽(tīng)?wèi)?yīng)用中的所有onClick點(diǎn)擊時(shí)間,或者監(jiān)聽(tīng)某些方法的運(yùn)行耗時(shí),如果在代碼中一個(gè)方法一個(gè)方法修改會(huì)很蛋疼,所以想通過(guò)Gradle插件來(lái)實(shí)現(xiàn)在應(yīng)用的編譯期間進(jìn)行代碼插入的功能。
1、AOP的概念
其實(shí)這已經(jīng)涉及到AOP(Aspect Oriented Programming),即面向切面編程,在編譯期間對(duì)代碼進(jìn)行動(dòng)態(tài)管理,以達(dá)到統(tǒng)一維護(hù)的目的。
AOP切面
舉個(gè)栗子,Android開(kāi)發(fā)我們都知道,在項(xiàng)目越來(lái)越大的時(shí)候,應(yīng)用可能被分解為多個(gè)模塊,如果你要往所有模塊的方法里頭加一句‘我是大傻叼'的Toast,那是不是得跪。所以最好的方式是想辦法在編譯的時(shí)候拿到所有方法,往方法里頭懟一個(gè)Toast,這樣還不會(huì)影響到運(yùn)行期間性能。
2、Transform
Android打包流程
如圖所示是Android打包流程,.java文件->.class文件->.dex文件,只要在紅圈處攔截住,拿到所有方法進(jìn)行修改完再放生就可以了,而做到這一步也不難,Google官方在Android Gradle的1.5.0 版本以后提供了 Transfrom API, 允許第三方 Plugin 在打包 dex 文件之前的編譯過(guò)程中操作 .class 文件,我們做的就是實(shí)現(xiàn)Transform進(jìn)行.class文件遍歷拿到所有方法,修改完成對(duì)原文件進(jìn)行替換。
/** * 自動(dòng)埋點(diǎn)追蹤,遍歷所有文件更換字節(jié)碼 */ public class AutoTransform extends Transform { @Override String getName() { return "AutoTrack" } @Override Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS } @Override Set<QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT } @Override boolean isIncremental() { return false } @Override public void transform( @NonNull Context context, @NonNull Collection<TransformInput> inputs, @NonNull Collection<TransformInput> referencedInputs, @Nullable TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException { //此處會(huì)遍歷所有文件 /**遍歷輸入文件*/ inputs.each { TransformInput input -> /** * 遍歷jar */ input.jarInputs.each { JarInput jarInput -> ... } /** * 遍歷目錄 */ input.directoryInputs.each { DirectoryInput directoryInput -> ... } } }
3、Gradle插件實(shí)現(xiàn)
通過(guò)Transform提供的api可以遍歷所有文件,但是要實(shí)現(xiàn)Transform的遍歷操作,得通過(guò)Gradle插件來(lái)實(shí)現(xiàn),關(guān)于Gradle插件的知識(shí)可以看相關(guān)博客,也可以直接看博主的項(xiàng)目 Luffy 。編寫(xiě)Gradle插件可能需要一點(diǎn)Goovy知識(shí),具體編寫(xiě)直接用java語(yǔ)言寫(xiě)也可以,Goovy是完全兼容java的,只截取插件入口部分實(shí)現(xiàn)PluginEntry.groovy
class PluginEntry implements Plugin<Project> { @Override void apply(Project project) { ... //使用Transform實(shí)行遍歷 def android = project.extensions.getByType(AppExtension) registerTransform(android) ... } def static registerTransform(BaseExtension android) { AutoTransform transform = new AutoTransform() android.registerTransform(transform) }
4、字節(jié)碼編寫(xiě)
完成上面的操作以后就剩下一件事了,那就是拿到.class文件了,大家都知道.class文件是字節(jié)碼格式的,操作起來(lái)難度是相當(dāng)于大的,所以需要一個(gè)字節(jié)碼操作庫(kù)來(lái)減輕難度,那就是ASM了。
4.1、ASM簡(jiǎn)介
ASM 可以直接產(chǎn)生二進(jìn)制的class 文件,也可以在增強(qiáng)既有類(lèi)的功能。Java class 被存儲(chǔ)在嚴(yán)格格式定義的 .class文件里,這些類(lèi)文件擁有足夠的元數(shù)據(jù)來(lái)解析類(lèi)中的所有元素:類(lèi)名稱(chēng)、方法、屬性以及 Java 字節(jié)碼(指令)。
4.2、具體使用ASM
ASM框架中的核心類(lèi)有以下幾個(gè):
- ClassReader:該類(lèi)用來(lái)解析編譯過(guò)的class字節(jié)碼文件。
- ClassWriter:該類(lèi)用來(lái)重新構(gòu)建編譯后的類(lèi),比如說(shuō)修改類(lèi)名、屬性以及方法,甚至可以生成新的類(lèi)的字節(jié)碼文件。
- ClassVisitor:主要負(fù)責(zé) “拜訪(fǎng)” 類(lèi)成員信息。其中包括標(biāo)記在類(lèi)上的注解,類(lèi)的構(gòu)造方法,類(lèi)的字段,類(lèi)的方法,靜態(tài)代碼塊。
- AdviceAdapter:實(shí)現(xiàn)了MethodVisitor接口,主要負(fù)責(zé) “拜訪(fǎng)” 方法的信息,用來(lái)進(jìn)行具體的方法字節(jié)碼操作。
- ClassVisitor的全部方法如下,按一定的次序來(lái)遍歷類(lèi)中的成員。
ClassVisitor的全部方法如下,按一定的次序來(lái)遍歷類(lèi)中的成員。
ClassVisitor全部api
在ClassVisitor中根據(jù)你的條件進(jìn)行判斷,滿(mǎn)足條件的類(lèi)才會(huì)修改其中方法,比如要統(tǒng)計(jì)點(diǎn)擊事件的話(huà),需要實(shí)現(xiàn)View$OnClickListener接口的類(lèi)才會(huì)遍歷其中的方法進(jìn)行操作。
class AutoClassVisitor extends ClassVisitor { AutoClassVisitor(final ClassVisitor cv) { super(Opcodes.ASM4, cv) } @Override void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { //進(jìn)行需要滿(mǎn)足類(lèi)的條件過(guò)濾 ... super.visit(version, access, name, signature, superName, interfaces) } @Override void visitInnerClass(String name, String outerName, String innerName, int access) { // 內(nèi)部類(lèi)信息 ... super.visitInnerClass(name, outerName, innerName, access) } @Override MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { // 拿到需要修改的方法,執(zhí)行修改操作 MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions) MethodVisitor adapter = null ... adapter = new AutoMethodVisitor(methodVisitor, access, name, desc) ... return methodVisitor } @Override void visitEnd() { //類(lèi)中成員信息遍歷介紹 ... super.visitEnd() } }
在MethodVisitor中根據(jù)對(duì)已經(jīng)拿到的方法進(jìn)行修改了。
MethodVisitor adapter = new AutoMethodVisitor(methodVisitor, access, name, desc) { boolean isAnnotation = false @Override protected void onMethodEnter() { super.onMethodEnter() //進(jìn)入方法時(shí)可以插入字節(jié)碼 ... } @Override protected void onMethodExit(int opcode) { super.onMethodExit(opcode) //退出方法前可以插入字節(jié)碼 ... } /** * 需要通過(guò)注解的方式加字節(jié)碼才會(huì)重寫(xiě)這個(gè)方法來(lái)進(jìn)行條件過(guò)濾 */ @Override AnnotationVisitor visitAnnotation(String des, boolean visible) { ... return super.visitAnnotation(des, visible) } }
5、實(shí)戰(zhàn)演練
以上就是總體的思路了,現(xiàn)在就通過(guò) Luffy 根據(jù)具體需求實(shí)戰(zhàn)一下,比如說(shuō)在onClick方法點(diǎn)擊的耗時(shí)(自動(dòng)埋點(diǎn)也是一樣的道理,只不過(guò)換了插樁的方法)。
5.1、插件配置
先打包一下插件到本地倉(cāng)庫(kù)進(jìn)行引用,在項(xiàng)目的根build.gradle加入插件的依賴(lài)
dependencies { classpath 'com.xixi.plugin:plugin:1.0.1-SNAPSHOT' }
在app的build.gradle中
apply plugin: 'apk.move.plugin' xiaoqingwa{ name = "小傻逼" isDebug = true //具體配置 matchData = [ //是否使用注解來(lái)找對(duì)應(yīng)方法 'isAnotation': false, //方法的匹配,可以通過(guò)類(lèi)名或者實(shí)現(xiàn)的接口名匹配 'ClassFilter': [ ['ClassName': null, 'InterfaceName':null, 'MethodName':null, 'MethodDes':null] ], //插入的字節(jié)碼,方法的執(zhí)行順序visitAnnotation->onMethodEnter->onMethodExit 'MethodVisitor':{ MethodVisitor methodVisitor, int access, String name, String desc -> MethodVisitor adapter = new AutoMethodVisitor(methodVisitor, access, name, desc) { boolean isAnnotation = false @Override protected void onMethodEnter() { super.onMethodEnter() //使用注解找對(duì)應(yīng)方法的時(shí)候得加這個(gè)判斷 } @Override protected void onMethodExit(int opcode) { super.onMethodExit(opcode) //使用注解找對(duì)應(yīng)方法的時(shí)候得加這個(gè)判斷 } /** * 需要通過(guò)注解的方式加字節(jié)碼才會(huì)重寫(xiě)這個(gè)方法來(lái)進(jìn)行條件過(guò)濾 */ @Override AnnotationVisitor visitAnnotation(String des, boolean visible) { return super.visitAnnotation(des, visible) } } return adapter } ] }
要是使用演示的話(huà),因?yàn)檫€沒(méi)上傳到j(luò)center庫(kù),所以只能本地倉(cāng)庫(kù)打包插件,記得要先把依賴(lài)都注釋掉,插件打包完成后再啟用,不然會(huì)編譯不過(guò)去的。
xiaoqingwa{}
里頭的配置信息先不用管,等會(huì)會(huì)講到,主要是為了能夠不修改插件進(jìn)行動(dòng)態(tài)更換插樁的方法。
5.2、應(yīng)用測(cè)試
插件配置好了之后就可以測(cè)試一下效果了,先寫(xiě)一個(gè)耗時(shí)統(tǒng)計(jì)的工具類(lèi)
TimeCache.java
/** * Author:xishuang * Date:2018.01.10 * Des:計(jì)時(shí)類(lèi),編譯器加入指定方法中 */ public class TimeCache { public static Map<String, Long> sStartTime = new HashMap<>(); public static Map<String, Long> sEndTime = new HashMap<>(); public static void setStartTime(String methodName, long time) { sStartTime.put(methodName, time); } public static void setEndTime(String methodName, long time) { sEndTime.put(methodName, time); } public static String getCostTime(String methodName) { long start = sStartTime.get(methodName); long end = sEndTime.get(methodName); long dex = end - start; return "method: " + methodName + " cost " + dex + " ns"; } }
大概思路就是使用HashMap來(lái)臨時(shí)保存對(duì)應(yīng)方法的時(shí)間,退出方法時(shí)獲取時(shí)間差。
在一個(gè)方法的前后插入時(shí)間統(tǒng)計(jì)的方法,這個(gè)具體的過(guò)程要怎么操作呢,因?yàn)閏lass文件是字節(jié)碼格式的,ASM也是進(jìn)行字節(jié)碼操作,所以必須先把插入的代碼轉(zhuǎn)換成字節(jié)碼先。這里推薦一個(gè)字節(jié)碼查看工具Java Bytecode Editor,導(dǎo)入.class文件就可以看到對(duì)應(yīng)字節(jié)碼了。
比如我們要插入的代碼如下:
private void countTime() { TimeCache.setStartTime("newFunc", System.currentTimeMillis()); TimeCache.setEndTime("newFunc", System.currentTimeMillis()); Log.d("耗時(shí)", TimeCache.getCostTime("newFunc")); }
先把.java文件編譯成.class文件,用Java Bytecode Editor打開(kāi)
插入代碼的字節(jié)碼
然后根據(jù)其用ASM提供的Api一一對(duì)應(yīng)的把代碼填進(jìn)來(lái)加到onMethodEnter和onMethodExit中。
//方法前加入 methodVisitor.visitMethodInsn methodVisitor.visitLdcInsn(name) methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false) methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "setStartTime", "(Ljava/lang/String;J)V", false) //方法后加入 methodVisitor.visitLdcInsn(name) methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false) methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "setEndTime", "(Ljava/lang/String;J)V", false) methodVisitor.visitLdcInsn("耗時(shí)") methodVisitor.visitLdcInsn(name) methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "getCostTime", "(Ljava/lang/String;)Ljava/lang/String;", false) methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false)
在app的build.gradle中配置得到的字節(jié)碼,最后設(shè)置一下過(guò)濾條件,最終的代碼如下:
build.gradle
xiaoqingwa{ name = "小傻逼" isDebug = true //具體配置 matchData = [ //是否使用注解來(lái)找對(duì)應(yīng)方法 'isAnotation': false, //方法的匹配,可以通過(guò)類(lèi)名或者實(shí)現(xiàn)的接口名匹配 'ClassFilter': [ ['ClassName': 'com.xishuang.plugintest.MainActivity', 'InterfaceName': 'android/view/View$OnClickListener', 'MethodName':'onClick', 'MethodDes':'(Landroid/view/View;)V'] ], //插入的字節(jié)碼,方法的執(zhí)行順序visitAnnotation->onMethodEnter->onMethodExit 'MethodVisitor':{ MethodVisitor methodVisitor, int access, String name, String desc -> MethodVisitor adapter = new AutoMethodVisitor(methodVisitor, access, name, desc) { boolean isAnnotation = false @Override protected void onMethodEnter() { super.onMethodEnter() //使用注解找對(duì)應(yīng)方法的時(shí)候得加這個(gè)判斷 // if (!isAnnotation){ // return // } methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/MainActivity", "notifyInsert", "()V", false) methodVisitor.visitLdcInsn(name) methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false) methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "setStartTime", "(Ljava/lang/String;J)V", false) } @Override protected void onMethodExit(int opcode) { super.onMethodExit(opcode) //使用注解找對(duì)應(yīng)方法的時(shí)候得加這個(gè)判斷 // if (!isAnnotation){ // return // } methodVisitor.visitLdcInsn(name) methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false) methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "setEndTime", "(Ljava/lang/String;J)V", false) methodVisitor.visitLdcInsn("耗時(shí)") methodVisitor.visitLdcInsn(name) methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "getCostTime", "(Ljava/lang/String;)Ljava/lang/String;", false) methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false) } /** * 需要通過(guò)注解的方式加字節(jié)碼才會(huì)重寫(xiě)這個(gè)方法來(lái)進(jìn)行條件過(guò)濾 */ @Override AnnotationVisitor visitAnnotation(String des, boolean visible) { // if (des.equals("Lcom/xishuang/annotation/AutoCount;")) { // println "注解匹配:" + des // isAnnotation = true // } return super.visitAnnotation(des, visible) } } return adapter } ] }
'isAnotation'
表示是否使用注解的方式找到對(duì)應(yīng)方法,這里false,因?yàn)槲覀儸F(xiàn)在是通過(guò)具體類(lèi)信息來(lái)判斷的。
'ClassFilter'
表示過(guò)濾條件,其中 'ClassName'
和 'InterfaceName'
用于判斷哪些類(lèi)中的方法可以遍歷其中的方法進(jìn)行匹配修改,不滿(mǎn)足的話(huà)就不會(huì)進(jìn)行方法名匹配了,這些感興趣的童鞋都可以改插件自定義擴(kuò)展。
'MethodName'
和 'MethodDes'
是方法名和方法描述符,可以唯一確定一個(gè)方法名,滿(mǎn)足類(lèi)過(guò)濾條件的就會(huì)進(jìn)行方法匹配,例如我們要統(tǒng)計(jì)的點(diǎn)擊事件 onClick(View v)
。
意思就是繼承自 android/view/View$OnClickListener
的類(lèi)或者類(lèi)名是 'com.xishuang.plugintest.MainActivity'
就可以進(jìn)行方法的遍歷,然后方法滿(mǎn)足 onClick(View v)
就會(huì)進(jìn)行代碼插入操作。
設(shè)置完之后rebuild一下就可以了,可以通過(guò)日志看下具體信息, isDebug = true
可以開(kāi)啟日志打印。
日志
通過(guò)日志可以看到我們?cè)O(shè)置的字節(jié)碼確實(shí)插樁成功,現(xiàn)在再看一下編譯后的文件驗(yàn)證一下,具體位置是:app\build\intermediates\transforms\AutoTrack\debug\folders
編譯后的.class文件
其中的notifyInsert()是我用來(lái)彈Toast額外調(diào)試用的,請(qǐng)忽略。在手機(jī)上點(diǎn)擊一下按鈕測(cè)試一下,發(fā)現(xiàn)確實(shí)記錄下點(diǎn)擊的耗時(shí)時(shí)間,完成。
5.3、注解匹配
除了以上的方式來(lái)查找修改的方法之外,還可以通過(guò)注解來(lái)查找,切換很簡(jiǎn)單,只需要改一下app的build.gradle文件就可以了,項(xiàng)目中也有栗子,添加了一個(gè)注解類(lèi)。
/** * Author:xishuang * Date:2018.1.9 * Des:時(shí)間統(tǒng)計(jì)注解 */ @Target(ElementType.METHOD) public @interface AutoCount { }
然后在對(duì)應(yīng)的方法上添加你自定義的注解
@AutoCount private void onClick() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } @AutoCount @Override public void onClick(View v) { if (v.getId() == R.id.button) { Toast.makeText(this, "我是按鈕", Toast.LENGTH_SHORT).show(); } }
修改一下build.gradle中的配置文件
xiaoqingwa{ name = "小傻逼" isDebug = true //具體配置 matchData = [ //是否使用注解來(lái)找對(duì)應(yīng)方法 'isAnotation': true, //方法的匹配,可以通過(guò)類(lèi)名或者實(shí)現(xiàn)的接口名匹配 'ClassFilter': [ ['ClassName': 'com.xishuang.plugintest.MainActivity', 'InterfaceName': 'android/view/View$OnClickListener', 'MethodName':'onClick', 'MethodDes':'(Landroid/view/View;)V'] ], //插入的字節(jié)碼,方法的執(zhí)行順序visitAnnotation->onMethodEnter->onMethodExit 'MethodVisitor':{ MethodVisitor methodVisitor, int access, String name, String desc -> MethodVisitor adapter = new AutoMethodVisitor(methodVisitor, access, name, desc) { boolean isAnnotation = false @Override protected void onMethodEnter() { super.onMethodEnter() //使用注解找對(duì)應(yīng)方法的時(shí)候得加這個(gè)判斷 if (!isAnnotation){ return } methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/MainActivity", "notifyInsert", "()V", false) methodVisitor.visitLdcInsn(name) methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false) methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "setStartTime", "(Ljava/lang/String;J)V", false) } @Override protected void onMethodExit(int opcode) { super.onMethodExit(opcode) //使用注解找對(duì)應(yīng)方法的時(shí)候得加這個(gè)判斷 if (!isAnnotation){ return } methodVisitor.visitLdcInsn(name) methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false) methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "setEndTime", "(Ljava/lang/String;J)V", false) methodVisitor.visitLdcInsn("耗時(shí)") methodVisitor.visitLdcInsn(name) methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "getCostTime", "(Ljava/lang/String;)Ljava/lang/String;", false) methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false) } /** * 需要通過(guò)注解的方式加字節(jié)碼才會(huì)重寫(xiě)這個(gè)方法來(lái)進(jìn)行條件過(guò)濾 */ @Override AnnotationVisitor visitAnnotation(String des, boolean visible) { if (des.equals("Lcom/xishuang/annotation/AutoCount;")) { println "注解匹配:" + des isAnnotation = true } return super.visitAnnotation(des, visible) } } return adapter } ] }
關(guān)鍵代碼在于把 'isAnotation'
設(shè)為 true
,然后在 visitAnnotation
方法中添加你的注解類(lèi)匹配,也就是這句 des.equals("Lcom/xishuang/annotation/AutoCount;")
代碼,注解類(lèi)的描述符,運(yùn)行效果和上面差不多,但是不會(huì)打印日志,因?yàn)橥ㄟ^(guò)注解來(lái)查找方法會(huì)遍歷每個(gè)方法,打印信息太多電腦會(huì)爆炸。
具體的信息可以看下源碼,已共享到github上,在這里講了下大概的思路和代碼框架,只是起到拋磚引玉的作用,更有趣的玩法大家可以自己修改一下插件來(lái)實(shí)現(xiàn)。
github地址: Luffy 。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
如何利用adb卸載手機(jī)預(yù)裝軟件(系統(tǒng)軟件)
對(duì)于Android手機(jī)通常有很多不必要的預(yù)置軟件,但是又無(wú)法卸載,占用桌面有很難受,所以本次使用adb工具來(lái)實(shí)現(xiàn)從電腦命令來(lái)卸載或停用軟件,下面這篇文章主要給大家介紹了關(guān)于如何利用adb卸載手機(jī)預(yù)裝軟件(系統(tǒng)軟件)的相關(guān)資料,需要的朋友可以參考下2022-09-09Android?Drawable代碼編寫(xiě)的新姿勢(shì)分享
Drawable是什么?按照字面翻譯,就是可繪制的,由于能夠繪制的東西很多,所以這個(gè)類(lèi)是一個(gè)抽象類(lèi),下面這篇文章主要給大家介紹了關(guān)于Android?Drawable代碼編寫(xiě)的新姿勢(shì),需要的朋友可以參考下2022-01-01OpenGL Shader實(shí)例分析(2)繪制心臟跳動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了OpenGL Shader實(shí)例分析第2篇,繪制心臟跳動(dòng)效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02淺談Android Studio導(dǎo)出javadoc文檔操作及問(wèn)題的解決
這篇文章主要介紹了淺談Android Studio導(dǎo)出javadoc文檔操作及問(wèn)題的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-03-03Android編程之View簡(jiǎn)單學(xué)習(xí)示例
這篇文章主要介紹了Android編程之View簡(jiǎn)單學(xué)習(xí)示例,以一個(gè)實(shí)例形式較為詳細(xì)的分析了View的功能、定義及使用技巧,需要的朋友可以參考下2015-10-10Android本地?cái)?shù)據(jù)存儲(chǔ)Room實(shí)踐和優(yōu)化技巧
本文詳細(xì)介紹了Android本地?cái)?shù)據(jù)存儲(chǔ)框架Room的使用,包括基本概念、核心組件、最佳實(shí)踐、優(yōu)化技巧等,幫助開(kāi)發(fā)者學(xué)習(xí)和掌握Room的使用方法,提升數(shù)據(jù)存儲(chǔ)效率和應(yīng)用性能2023-04-04Flutter進(jìn)階之實(shí)現(xiàn)動(dòng)畫(huà)效果(八)
這篇文章主要為大家詳細(xì)介紹了Flutter進(jìn)階之實(shí)現(xiàn)動(dòng)畫(huà)效果的第八篇,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08androidstudio3.0使用butterknife報(bào)錯(cuò)解決的解決方法
這篇文章主要介紹了androidstudio3.0使用butterknife報(bào)錯(cuò)解決的解決方法,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2018-01-01