Android面向切面AOP架構設計簡析
按照慣例,談一個框架時我們先說明一下這東西到底是啥、幹什麼的,首先AOP面向切面和我們通常意義上寫的程式碼不太一樣,Java是OOP面向物件,所有的程式碼都是符合某個功能的,是分門別類好的,但是我們在實際的安卓開發過程中OOP的設計思想是比較難處理一些問題的,比如模組埋點、鑑權以及一些簡單但是重複性比較高的程式碼,如我們要檢視個人資料頁面就必須先登入,檢視個人訊息也需要登入。
if(isLogin){ 你的業務邏輯 }else{ 開啟登入頁面 }
像上面的這種程式碼會大量的出現在我們的專案中,當然這是比較不太優雅的實現方法,還有像程式碼埋點,如果說使用者登入這個還能勉強做個工具類,但是埋點就真的是毫無辦法了,這個時候我們用到AOP就能夠優雅的解決問題了。
這個時候就有必要提到一個框架AspectJ,它可以在程式碼編譯期插入程式碼來實現你的業務需求,這是我的理解,當然如果在網上覆制一大段關於它的描述沒意思,概念都不是人看的,直接上程式碼看執行效果,相信大家會有一個比較清晰的認識。
首先我們需要對Gradle進行配置
// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() jcenter() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:3.1.2' //下面兩個是框架的依賴 classpath 'org.aspectj:aspectjtools:1.8.9' classpath 'org.aspectj:aspectjweaver:1.8.9' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() jcenter() } } task clean(type: Delete) { delete rootProject.buildDir }
上面是Project的gradle的依賴,以下是app的gradle配置
apply plugin: 'com.android.application' android { compileSdkVersion 28 defaultConfig { applicationId "demo.zhongshi.com.myapplication" minSdkVersion 15 targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } buildToolsVersion '28.0.2' } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation 'com.android.support:appcompat-v7:28.0.0-rc01' implementation 'com.android.support.constraint:constraint-layout:1.1.2' // 只需下一行的依賴 implementation 'org.aspectj:aspectjrt:1.8.9' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' } // 下面是AspectJ的編譯配置檔案 import org.aspectj.bridge.IMessage import org.aspectj.bridge.MessageHandler import org.aspectj.tools.ajc.Main final def log = project.logger android.applicationVariants.all{ variant -> if (!variant.buildType.isDebuggable()) { log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.") return } JavaCompile javaCompile = variant.javaCompiler 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 } } } }
眾所周知,我們用javac可以把java編譯成class檔案,當然我們如果一個類只寫了一行程式碼,class檔案是不會生成其他無關的位元組碼的,這是常識,但是AspectJ是可以生成屬於自己規則的位元組碼,它是遵循java編譯的規則並做了自己的處理,所使用的編譯器是 Ajc,整個的使用步驟可以分為三個部分,切點、切面、處理,我們先通過一個小demo來看下它的具體玩法,然後再談這個問題。
新建一個註解,標識為執行期作用,用於對方法切點
/** * 利用註解來切點標示方法,作用在執行期 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface CheckLogin { String value(); }

主頁.png
然後我們看下下面按鈕的點選方法處理
@CheckLogin("startPersonalData") private void startPersonalData() { Log.e("--->","進入個人資料頁面"); }
最後是AspectJ對切點和切面的處理
@Aspect public class CheckLoginAspectJ { /** * 找到指定註解的切點 */ @Pointcut("execution(@demo.zhongshi.com.myapplication.CheckLogin * *(..))") public void executeCheckLogin(){} /** * 切面 * @param point * @return * @throws Throwable */ @Around("executeCheckLogin()") public Object checkLogin(ProceedingJoinPoint point) throws Throwable{ MethodSignature signature = (MethodSignature) point.getSignature(); CheckLogin checkLogin = signature.getMethod().getAnnotation(CheckLogin.class); if(null != checkLogin){ boolean isLogin = Constants.isLogin(); if(isLogin){ Log.e("--->", "當前已登入"); return point.proceed(); }else { Log.e("--->", "請登陸賬號"); Context context = (Context) point.getThis(); Intent intent = new Intent(context, LoginActivity.class); context.startActivity(intent); return null; } } Log.e("--->", "註解為空"); return point.proceed(); } }
當沒有登入時的日誌輸出和顯示頁面為

未登陸時的列印.png

登入頁面.png
然後點選上面的按鈕,手動將變數設定為true

登入後的日誌輸出.png
相信看到這裡大家應該有了比較清晰的認識,已經晚上11:42了,明天還要上班,就不深入說明這個框架了,明後兩天再深入剖析一下框架的用法和實現原理。