在Android項(xiàng)目中使用AspectJ的詳細(xì)攻詻
AOP
全稱“Aspect Oriented Programming”,面向切面編程,由于面向?qū)ο蟮乃枷胍蟾邇?nèi)聚,低耦合的風(fēng)格,使模塊代碼間的可見(jiàn)性變差,對(duì)于埋點(diǎn),日志輸出等需求,就會(huì)變的十分復(fù)雜,如果手動(dòng)編寫代碼,入侵性很大,不利于擴(kuò)展,AOP應(yīng)運(yùn)而生。
AspectJ
AspectJ實(shí)際上是對(duì)AOP編程的實(shí)踐,目前還有很多的AOP實(shí)現(xiàn),如ASMDex,但筆者選用的是AspectJ。
使用場(chǎng)景
當(dāng)我們需要在某個(gè)方法運(yùn)行前和運(yùn)行后做一些處理時(shí),便可使用AOP技術(shù)。具體有:
- 統(tǒng)計(jì)埋點(diǎn)
- 日志打印/打點(diǎn)
- 數(shù)據(jù)校驗(yàn)
- 行為攔截
- 性能監(jiān)控
- 動(dòng)態(tài)權(quán)限控制
AOP(aspect-oriented programming),指的是面向切面編程。而AspectJ是實(shí)現(xiàn)AOP的其中一款框架,內(nèi)部通過(guò)處理字節(jié)碼實(shí)現(xiàn)代碼注入。
AspectJ從2001年發(fā)展至今,已經(jīng)非常成熟穩(wěn)定,同時(shí)使用簡(jiǎn)單是它的一大優(yōu)點(diǎn)。至于它的使用場(chǎng)景,可以看本文中的一些小例子,獲取能給你啟發(fā)。
1.集成AspectJ
使用插件gradle-android-aspectj-plugin
這種方式接入簡(jiǎn)單。但是此插件截止目前已經(jīng)一年多沒(méi)有維護(hù)了,考慮到AGP的兼容性,害怕以后無(wú)法使用。這里就不推薦了。(這里存在特殊情況,文章后面會(huì)提到。)
常規(guī)的Gradle 配置方式
這種方法相對(duì)配置會(huì)多一些,但相對(duì)可控。
首先在項(xiàng)目根目錄的build.gradle中添加:
classpath "com.android.tools.build:gradle:4.2.1" classpath 'org.aspectj:aspectjtools:1.9.6'
然后在app的build.gradle中添加:
dependencies { ... implementation 'org.aspectj:aspectjrt:1.9.6' } import org.aspectj.bridge.IMessage import org.aspectj.bridge.MessageHandler import org.aspectj.tools.ajc.Main final def log = project.logger final def variants = project.android.applicationVariants variants.all { variant -> // 注意這里控制debug下生效,可以自行控制是否生效 if (!variant.buildType.isDebuggable()) { log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.") return } JavaCompile javaCompile = variant.javaCompileProvider.get() javaCompile.doLast { String[] args = ["-showWeaveInfo", "-1.8", "-inpath", javaCompile.destinationDir.toString(), "-aspectpath", javaCompile.classpath.asPath, "-d", javaCompile.destinationDir.toString(), "-classpath", javaCompile.classpath.asPath, "-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)] log.debug "ajc args: " + Arrays.toString(args) MessageHandler handler = new MessageHandler(true) new Main().run(args, handler) for (IMessage message : handler.getMessages(null, true)) { switch (message.getKind()) { case IMessage.ABORT: case IMessage.ERROR: case IMessage.FAIL: log.error message.message, message.thrown break case IMessage.WARNING: log.warn message.message, message.thrown break case IMessage.INFO: log.info message.message, message.thrown break case IMessage.DEBUG: log.debug message.message, message.thrown break } } } }
在 module 使用的話一樣需要添加配置代碼(略有不同):
dependencies { ... implementation 'org.aspectj:aspectjrt:1.9.6' } import org.aspectj.bridge.IMessage import org.aspectj.bridge.MessageHandler import org.aspectj.tools.ajc.Main final def log = project.logger android.libraryVariants.all{ variant -> if (!variant.buildType.isDebuggable()) { log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.") return } JavaCompile javaCompile = variant.javaCompileProvider.get() javaCompile.doLast { String[] args = ["-showWeaveInfo", "-1.8", "-inpath", javaCompile.destinationDir.toString(), "-aspectpath", javaCompile.classpath.asPath, "-d", javaCompile.destinationDir.toString(), "-classpath", javaCompile.classpath.asPath, "-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)] log.debug "ajc args: " + Arrays.toString(args) MessageHandler handler = new MessageHandler(true) new Main().run(args, handler) for (IMessage message : handler.getMessages(null, true)) { switch (message.getKind()) { case IMessage.ABORT: case IMessage.ERROR: case IMessage.FAIL: log.error message.message, message.thrown break case IMessage.WARNING: log.warn message.message, message.thrown break case IMessage.INFO: log.info message.message, message.thrown break case IMessage.DEBUG: log.debug message.message, message.thrown break } } } }
2.AspectJ基礎(chǔ)語(yǔ)法
Join Points
連接點(diǎn),用來(lái)連接我們需要操作的位置。比如連接普通方法、構(gòu)造方法還是靜態(tài)初始化塊等位置,以及是調(diào)用方法外部還是調(diào)用方法內(nèi)部。常用類型有Method call
、Method execution
、Constructor call
、Constructor execution
等。
Pointcuts
切入點(diǎn),是帶條件的Join Points,確定切入點(diǎn)位置。
Pointcuts語(yǔ)法 | 說(shuō)明 |
---|---|
execution(MethodPattern) | 方法執(zhí)行 |
call(MethodPattern) | 方法被調(diào)用 |
execution(ConstructorPattern) | 構(gòu)造方法執(zhí)行 |
call(ConstructorPattern) | 構(gòu)造方法被調(diào)用 |
get(FieldPattern) | 讀取屬性 |
set(FieldPattern) | 設(shè)置屬性 |
staticinitialization(TypePattern) | static 塊初始化 |
handler(TypePattern) | 異常處理 |
execution和call的區(qū)別如下圖:
Pattern規(guī)則如下:
Pattern | 規(guī)則(注意空格) |
---|---|
MethodPattern | [@注解] [訪問(wèn)權(quán)限] 返回值類型 [類名.]方法名(參數(shù)) [throws 異常類型] |
ConstructorPattern | [@注解] [訪問(wèn)權(quán)限] [類名.]new(參數(shù)) [throws 異常類型] |
FieldPattern | [@注解] [訪問(wèn)權(quán)限] 變量類型 [類名.]變量名 |
TypePattern | * 單獨(dú)使用事表示匹配任意類型,.. 匹配任意字符串,.. 單獨(dú)使用時(shí)表示匹配任意長(zhǎng)度任意類型,+ 匹配其自身及子類,還有一個(gè) ... 表示不定個(gè)數(shù)。也可以使用&&,||,! 進(jìn)行邏輯運(yùn)算。 |
- 上表中中括號(hào)為可選項(xiàng),沒(méi)有可以不寫
- 方法匹配例子:
java.*.Date:可以表示java.sql.Date,也可以表示java.util.Date Test*:可以表示TestBase,也可以表示TestDervied java..*:表示java任意子類 java..*Model+:表示Java任意package中名字以Model結(jié)尾的子類,比如TabelModel,TreeModel 等
參數(shù)匹配例子:
(int, char):表示參數(shù)只有兩個(gè),并且第一個(gè)參數(shù)類型是int,第二個(gè)參數(shù)類型是char (String, ..):表示至少有一個(gè)參數(shù)。并且第一個(gè)參數(shù)類型是String,后面參數(shù)類型不限. ..代表任意參數(shù)個(gè)數(shù)和類型 (Object ...):表示不定個(gè)數(shù)的參數(shù),且類型都是Object,這里的...不是通配符,而是Java中代表不定參數(shù)的意思
Advice
用來(lái)指定代碼插入到Pointcuts的什么位置。
Advice | 說(shuō)明 |
---|---|
@Before | 在執(zhí)行JPoint之前 |
@After | 在執(zhí)行JPoint之后 |
@AfterReturning | 方法執(zhí)行后,返回結(jié)果后再執(zhí)行。 |
@AfterThrowing | 處理未處理的異常。 |
@Around | 可以替換原代碼。如果需要執(zhí)行原代碼,可以使用ProceedingJoinPoint#proceed()。 |
After、Before 示例
這里我們實(shí)現(xiàn)一個(gè)功能,在所有Activity的onCreate方法中添加Trace方法,來(lái)統(tǒng)計(jì)onCreate方法耗時(shí)。
@Aspect // <-注意添加,才會(huì)生效參與編譯 public class TraceTagAspectj { @Before("execution(* android.app.Activity+.onCreate(..))") public void before(JoinPoint joinPoint) { Trace.beginSection(joinPoint.getSignature().toString()); } @After("execution(* android.app.Activity+.onCreate(..))") public void after() { Trace.endSection(); } }
編譯后的class代碼如下:
可以看到經(jīng)過(guò)處理后,它并不會(huì)直接把 Trace 函數(shù)直接插入到代碼中,而是經(jīng)過(guò)一系列自己的封裝。如果想針對(duì)所有的函數(shù)都做插樁,AspectJ 會(huì)帶來(lái)不少的性能影響。
不過(guò)大部分情況,我們可能只會(huì)插樁某一小部分函數(shù),這樣 AspectJ 帶來(lái)的性能影響就可以忽略不計(jì)了。
AfterReturning示例
獲取切點(diǎn)的返回值,比如這里我們獲取TextView,打印它的text值。
private TextView testAfterReturning() { return findViewById(R.id.tv); }
@Aspect public class TextViewAspectj { @AfterReturning(pointcut = "execution(* *..*.testAfterReturning())", returning = "textView") // "textView"必須和下面參數(shù)名稱一樣 public void getTextView(TextView textView) { Log.d("weilu", "text--->" + textView.getText().toString()); } }
編譯后的class代碼如下:
log打?。?br />
使用@AfterReturning
你可以對(duì)方法的返回結(jié)果做一些修改(注意是“=”賦值,String無(wú)法通過(guò)此方法修改)。
AfterThrowing示例
當(dāng)方法執(zhí)行出現(xiàn)異常,且異常沒(méi)有處理時(shí),可以使用@AfterThrowing
。比如下面的例子中,我們捕獲異常并上報(bào)(這里用log輸出實(shí)現(xiàn))
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); testAfterThrowing(); } private void testAfterThrowing() { TextView textView = null; textView.setText("aspectj"); } }
@Aspect public class ReportExceptionAspectj { @AfterThrowing(pointcut = "call(* *..*.testAfterThrowing())", throwing = "throwable") // "throwable"必須和下面參數(shù)名稱一樣 public void reportException(Throwable throwable) { Log.e("weilu", "throwable--->" + throwable); } }
編譯后的class代碼如下:
log打?。?br />
這里要注意的是,程序最終還是會(huì)崩潰,因?yàn)樽詈髨?zhí)行了throw var3
。如果你想不崩潰,可以使用@Around。
Around示例
接著上面的例子,我們這次直接try catch住異常代碼:
@Aspect public class TryCatchAspectj { @Pointcut("execution(* *..*.testAround())") public void methodTryCatch() { } @Around("methodTryCatch()") public void aroundTryJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable { try { joinPoint.proceed(); // <- 調(diào)用原代碼 } catch (Exception e) { e.printStackTrace(); } } }
編譯后的class代碼如下:
@Around
明顯更加靈活,我們可以自定義,實(shí)現(xiàn)"偷梁換柱"的效果,比如上面提到的替換方法的返回值。
3.進(jìn)階
withincode
withincode
表示某個(gè)方法執(zhí)行過(guò)程中涉及到的JPoint,通常用來(lái)過(guò)濾切點(diǎn)。例如我們有一個(gè)Person對(duì)象:
public class Person { private String name; private int age; public Person() { this.name = "weilu"; this.age = 18; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
Person
對(duì)象中有兩處set age的地方,如果我們只想讓構(gòu)造方法的生效,讓setAge
方法失效,可以使用@Around("execution(* com.weilu.aspectj.demo.Person.setAge(..))")
不過(guò)如果有更多處set age的地方,我們這樣一個(gè)個(gè)去匹配就很麻煩。
這里就可以考慮使用set
這個(gè)Pointcuts:
public class FieldAspectJ { @Around("set(int com.weilu.aspectj.demo.Person.age)") public void aroundFieldSet(ProceedingJoinPoint joinPoint) throws Throwable { Log.e("weilu", "around->" + joinPoint.getTarget().toString() + "#" + joinPoint.getSignature().getName()); } }
由于set(FieldPattern)
的FieldPattern限制,不能指定參數(shù),這樣會(huì)將所有的set age都切入:
這時(shí)就可以使用withincode
添加過(guò)濾條件:
@Aspect public class FieldAspectJ { @Pointcut("!withincode(com.weilu.aspectj.demo.Person.new())") public void invokePerson() { } @Around("set(int com.weilu.aspectj.demo.Person.age) && invokePerson()") public void aroundFieldSet(ProceedingJoinPoint joinPoint) throws Throwable { Log.e("weilu", "around->" + joinPoint.getTarget().toString() + "#" + joinPoint.getSignature().getName()); } }
結(jié)果如下:
還有一個(gè)within
,它和withincode
類似。不同的是,它的范圍是類,而withincode
是方法。例如:within(com.weilu.activity.*)
表示此包下任意的JPoint。
args
用來(lái)指定當(dāng)前執(zhí)行方法的參數(shù)條件。比如上一個(gè)例子中,如果需要指定第一個(gè)參數(shù)是int,后面參數(shù)不限。就可以這樣寫。
@Around("execution(* com.weilu.aspectj.withincode.Person.setAge(..)) && args(int,..)")
cflow
cflow是call flow的意思,cflow的條件是一個(gè)
pointcut
舉一個(gè)例子來(lái)說(shuō)明一下它的用途,a方法中調(diào)用了b、c、d方法。此時(shí)要統(tǒng)計(jì)各個(gè)方法的耗時(shí),如果按之前掌握的語(yǔ)法,我們最多需要寫四個(gè)Pointcut,方法越多越麻煩。
使用cflow,我們可以方便的掌握方法的“調(diào)用流”。我們測(cè)試方法如下:
private void test() { testAfterReturning(); testAround(); testWithInCode(); }
實(shí)現(xiàn)如下:
@Aspect public class TimingAspect { @Around("execution(* *(..)) && cflow(execution(* com.weilu.aspectj.demo.MainActivity.test(..)))") public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = currentTimeMillis(); Object result = joinPoint.proceed(); long endTime = currentTimeMillis(); Log.e("weilu", joinPoint.getSignature().toString() + " -> " + (endTime - startTime) + " ms"); return result; } }
cflow(execution(* com.weilu.aspectj.demo.MainActivity.test(..)))
表示調(diào)用test方法時(shí)所包含的JPoint,包括自身JPoint。
execution(* *(..))
的作用是去除TimingAspect
自身的代碼,避免自己攔截自己,形成死循環(huán)。
log結(jié)果如下:
還有一個(gè)cflowbelow
,它和cflow
類似。不同的是,它不包括自身JPoint。也就是例子中不會(huì)獲取test方法的耗時(shí)。
4.實(shí)戰(zhàn) 攔截點(diǎn)擊
攔截點(diǎn)擊的目的是避免因快速點(diǎn)擊控件,導(dǎo)致重復(fù)執(zhí)行點(diǎn)擊事件。例如打開(kāi)多次頁(yè)面,彈出多次彈框,請(qǐng)求多次接口,我之前發(fā)現(xiàn)在部分機(jī)型上,很容易復(fù)現(xiàn)此類情況。所以避免抖動(dòng)這算是項(xiàng)目中的一個(gè)常見(jiàn)需求。
例如butterknife
中就自帶DebouncingOnClickListener
來(lái)避免此類問(wèn)題。
如果你已不在使用butterknife
,也可以復(fù)制這段代碼。一個(gè)個(gè)的替換已有的View.OnClickListener
。還有以前使用Rxjava操作符來(lái)處理防抖。但這些方式侵入式大且替換的工作量也大。
這種場(chǎng)景就可以考慮AOP的方式處理。攔截onClick方法,判斷是否可以點(diǎn)擊。
@Aspect public class InterceptClickAspectJ { // 最后一次點(diǎn)擊的時(shí)間 private Long lastTime = 0L; // 點(diǎn)擊間隔時(shí)長(zhǎng) private static final Long INTERVAL = 300L; @Around("execution(* android.view.View.OnClickListener.onClick(..))") public void clickIntercept(ProceedingJoinPoint joinPoint) throws Throwable { // 大于間隔時(shí)間可點(diǎn)擊 if (System.currentTimeMillis() - lastTime >= INTERVAL) { // 記錄點(diǎn)擊時(shí)間 lastTime = System.currentTimeMillis(); // 執(zhí)行點(diǎn)擊事件 joinPoint.proceed(); } else { Log.e("weilu", "重復(fù)點(diǎn)擊"); } } }
實(shí)現(xiàn)代碼很簡(jiǎn)單,效果如下:
考慮到有些view的點(diǎn)擊事件不需要防抖,例如checkBox。否則checkBox狀態(tài)變了,但事件沒(méi)有執(zhí)行。我們可以定義一個(gè)注解,用withincode
過(guò)濾有此注解的方法。具體需求可以根據(jù)實(shí)際項(xiàng)目自行拓展,這里僅提供思路。
埋點(diǎn)
前面的例子中都是無(wú)侵入的方式使用AspectJ。這里說(shuō)一下侵入式的方式,簡(jiǎn)單說(shuō)就是使用自定義注解,用注解作為切入點(diǎn)的規(guī)則。(其實(shí)也可以自定義一種方法命名,來(lái)當(dāng)做切入規(guī)則)
首先定義兩個(gè)注解,一個(gè)用來(lái)傳固定參數(shù)比如eventName、eventId,同時(shí)負(fù)責(zé)當(dāng)做切入點(diǎn),一個(gè)用來(lái)傳動(dòng)態(tài)參數(shù)的key。
@Retention(RetentionPolicy.RUNTIME) public @interface TrackEvent { /** * 事件名稱 */ String eventName() default ""; /** * 事件id */ String eventId() default ""; } @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface TrackParameter { String value() default ""; }
Aspectj代碼如下:
@Aspect public class TrackEventAspectj { @Around("execution(@com.weilu.aspectj.tracking.TrackEvent * *(..))") public void trackEvent(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); // 獲取方法上的注解 TrackEvent trackEvent = signature.getMethod().getAnnotation(TrackEvent.class); String eventName = trackEvent.eventName(); String eventId = trackEvent.eventId(); JSONObject params = new JSONObject(); params.put("eventName", eventName); params.put("eventId", eventId); // 獲取方法參數(shù)的注解 Annotation[][] parameterAnnotations = signature.getMethod().getParameterAnnotations(); if (parameterAnnotations.length != 0) { int i = 0; for (Annotation[] parameterAnnotation : parameterAnnotations) { for (Annotation annotation : parameterAnnotation) { if (annotation instanceof TrackParameter) { // 獲取key value String key = ((TrackParameter) annotation).value(); params.put(key, joinPoint.getArgs()[i++]); } } } } // 上報(bào) Log.e("weilu", "上報(bào)數(shù)據(jù)---->" + params.toString()); try { joinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } } }
使用方法:
@TrackEvent(eventName = "點(diǎn)擊按鈕", eventId = "100") private void trackMethod(@TrackParameter("uid") int uid, String name) { Intent intent = new Intent(this, KotlinActivity.class); intent.putExtra("uid", uid); intent.putExtra("name", name); startActivity(intent); } trackMethod(10, "weilu");
結(jié)果如下:
由于匹配key value的代碼問(wèn)題,建議將需要?jiǎng)討B(tài)傳入的參數(shù)都寫在前面,避免下標(biāo)越界。
還有一些使用場(chǎng)景,比如權(quán)限控制??偨Y(jié)一下,AOP適合將一些通用邏輯分離出來(lái),然后通過(guò)AOP將此部分注入到業(yè)務(wù)代碼中。這樣我們可以更加注重業(yè)務(wù)的實(shí)現(xiàn),代碼也顯得清晰起來(lái)。
5.其他問(wèn)題 lambda
如果我們代碼中有使用lambda,例如點(diǎn)擊事件會(huì)變?yōu)椋?/p>
tv.setOnClickListener(v -> Log.e("weilu", "點(diǎn)擊事件執(zhí)行"));
這樣之前的點(diǎn)擊切入點(diǎn)就無(wú)效了,這里涉及到D8這個(gè)脫糖工具和invokedynamic字節(jié)碼指令相關(guān)知識(shí),這里我也無(wú)法說(shuō)的清楚詳細(xì)。簡(jiǎn)單說(shuō)使用lambda會(huì)生成lambda$
開(kāi)頭的中間方法,所以只能如下處理:
@Around("execution(* *..lambda$*(android.view.View))")
這種暫時(shí)處理起來(lái)比較麻煩,且可以看出容錯(cuò)率也比較低,很容易切入其他無(wú)關(guān)方法,所以建議AOP不要使用lambda。
配置
一開(kāi)始介紹了兩種配置,雖說(shuō)AspectJX插件最近不太維護(hù)了,但是它的支持了AAR、JAR及Kotlin的切入,而默認(rèn)僅是對(duì)自己的代碼進(jìn)行切入。
在AspectJ常規(guī)配置中有這樣的代碼:"-inpath", javaCompile.destinationDir.toString(),代表只對(duì)源文件進(jìn)行織入。在查看Aspectjx源碼時(shí),發(fā)現(xiàn)在“-inputs”配置加入了.jar文件,使得class類可以被織入代碼。這么理解來(lái)看,AspectJ也是支持對(duì)class文件的織入的,只是需要對(duì)它進(jìn)行相關(guān)的配置,而配置比較繁瑣,所以誕生了AspectJx等插件。
例如Kotlin在需要在常規(guī)的Gradle 配置上增加如下配置:
def buildType = variant.buildType.name String[] kotlinArgs = [ "-showWeaveInfo", "-1.8", "-inpath", project.buildDir.path + "/tmp/kotlin-classes/" + buildType, "-aspectpath", javaCompile.classpath.asPath, "-d", project.buildDir.path + "/tmp/kotlin-classes/" + buildType, "-classpath", javaCompile.classpath.asPath, "-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)] MessageHandler handler = new MessageHandler(true) new Main().run(kotlinArgs, handler)
同時(shí)注意用kotlin寫對(duì)應(yīng)的Aspect類,畢竟你需要注入的是kotlin代碼,用java的肯定不行,但是反過(guò)來(lái)卻可行。
建議有AAR、JAR及Kotlin需求的使用插件方式,即使后期無(wú)人維護(hù),可自行修改源碼適配GAP,相對(duì)難度不大。
這部分內(nèi)容較多同時(shí)也比較枯燥,斷斷續(xù)續(xù)整理了一周的時(shí)間。基本介紹了AspectJ在Android 中的配置,以及常用的語(yǔ)法與使用場(chǎng)景。對(duì)于應(yīng)用AspectJ來(lái)說(shuō)夠用了。
最后本篇涉及的代碼都已上傳至Github,有興趣的同學(xué)可以用做參考。
參考
AOP之AspectJ在Android中的應(yīng)用
AOP 之 AspectJ 全面剖析 in Android
編譯插樁的三種方法:AspectJ、ASM、ReDex
Android 引入AspectJ的記錄
以上就是在Android項(xiàng)目中的使用AspectJ的詳細(xì)攻詻的詳細(xì)內(nèi)容,更多關(guān)于AspectJ在android中使用的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Android notifyDataSetChanged() 動(dòng)態(tài)更新ListView案例詳解
- 解決java.lang.NoClassDefFoundError: android.support.v4.animation.AnimatorCompatHelper問(wèn)題
- AndroidStudio報(bào)錯(cuò)Emulator:PANIC:Cannot find AVD system path. Please define ANDROID_SDK_ROOT(解決方案)
- Android實(shí)現(xiàn)快速滾動(dòng)FastScrollView效果
- 捕獲與解析Android NativeCrash
- Android AS創(chuàng)建自定義布局案例詳解
相關(guān)文章
Android連接MySQL數(shù)據(jù)庫(kù)實(shí)現(xiàn)方法詳解
這篇文章主要介紹了Android連接MySQL數(shù)據(jù)庫(kù)實(shí)現(xiàn)方法,在Android應(yīng)用程序中連接MySQL數(shù)據(jù)庫(kù)可以幫助開(kāi)發(fā)人員實(shí)現(xiàn)更豐富的數(shù)據(jù)管理功能,而且在Android中操作數(shù)據(jù)庫(kù)真的太智能了,需要的朋友可以參考下2024-02-02Android RatingBar星星評(píng)分控件實(shí)例代碼
本文通過(guò)實(shí)例代碼給大家介紹了Android RatingBar星星評(píng)分控件,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下吧2017-06-06淺談Android PathMeasure詳解和應(yīng)用
本篇文章主要介紹了淺談Android PathMeasure詳解和應(yīng)用,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-01-01android-獲取網(wǎng)絡(luò)時(shí)間、獲取特定時(shí)區(qū)時(shí)間、時(shí)間同步的方法
本篇文章主要介紹了android-獲取網(wǎng)絡(luò)時(shí)間、獲取特定時(shí)區(qū)時(shí)間、時(shí)間同步,小編覺(jué)得不錯(cuò),現(xiàn)在就分享給大家,有興趣的可以了解一下。2016-12-12Android播放多張圖片形成的一個(gè)動(dòng)畫(huà)示例
這篇文章主要介紹了Android播放多張圖片形成的一個(gè)動(dòng)畫(huà)實(shí)現(xiàn)方法,結(jié)合實(shí)例形式分析了Android逐幀播放動(dòng)畫(huà)圖片及ImageView控件的相關(guān)使用技巧,需要的朋友可以參考下2016-10-10Android RecyclerView選擇多個(gè)item的實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了Android RecyclerView選擇多個(gè)item的實(shí)現(xiàn)代碼,仿網(wǎng)易新聞客戶端頻道選擇效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-02-02Android生存指南之:開(kāi)發(fā)中的注意事項(xiàng)
本篇文章是對(duì)在Android開(kāi)發(fā)中的一些注意事項(xiàng),需要的朋友可以參考下2013-05-05android實(shí)現(xiàn)文字水印效果 支持多行水印
這篇文章主要為大家詳細(xì)介紹了android添加文字水印,并支持多行水印,自定義角度和文字大小,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-10-10Android View事件分發(fā)和消費(fèi)源碼簡(jiǎn)單理解
這篇文章主要介紹了Android View事件分發(fā)和消費(fèi)源碼簡(jiǎn)單理解的相關(guān)資料,需要的朋友可以參考下2017-07-07