咱們應該對這樣的需求不陌生:App容許用戶以遊客的身份瀏覽,在用戶點擊某些操做時,先判斷用戶的登陸狀態,若是已登陸,則執行對應操做,若是沒登陸,則跳轉至登陸頁面要求登陸。只是這樣倒也不難處理,無非是加個if判斷,來決定是跳登陸仍是直接執行對應操做。可是產品經理可能又說了,登陸成功後不能直接回到原來的頁面就完了,你還要自動繼續以前中斷的操做。稍加思考,也還行,跳頁面的時候把要執行的操做記錄一下,用startActivityForResult跳轉,要求登陸模塊把結果返回來,而後咱們在onActivityForResult中獲取登陸結果,若是登陸成功了再根據跳轉前的記錄,執行以前中斷的操做。這麼看好像也不存在難度上的問題,只是繁瑣,過於繁瑣,尤爲是頁面上有多個須要登陸的操做時,在onActivityForResult裏的判斷要寫炸了,這一點咱們在下面的例子中能夠看到。java
先上項目地址,建議配合項目代碼看此文章android
頁面是上面這樣,咱們的app有登陸模塊(loginmodule)、實名認證模塊(authmodule)、vip模塊(vipmodule),「點贊」要求用戶登陸,並在登陸成功後自動點贊;「評論」操做首先要求用戶是登陸狀態,其次還要是進行了實名認證狀態;「屏蔽」要求用戶vip等級達到3,若是不到3會跳轉到購買vip頁面,在購買的時候再驗證登陸狀態,一樣,購買成功後自動執行屏蔽操做。git
咱們的幾種方式都是基於startActivityForResult和onActivityResult來實現的,在各個模塊內部有多少個頁面,如何跳轉咱們都不關心,咱們只關心如下幾點:github
For example, if the activity you are launching uses the singleTask launch mode, it will not run in your task and thus you will immediately receive a cancel result.編程
基於此緣由,能夠在manifest中設置爲標準模式,在返回入口Activity的時候給跳轉的intent設置flags來達到和singleTask同樣的效果。具體可參考loginmodule的代碼bash
val intent = Intent(this,InputAccountActivity::class.java) intent.putExtra("loginResult",true) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) startActivity(intent) 複製代碼
下面咱們會由繁到簡介紹多種方式來處理這種業務流程,最終的方案只需在方法上加幾個註解就ok了。markdown
最原始的,使用startActivityForResult跳轉,並在onActivityResult裏處理結果app
這是recyclerView子view的點擊事件代碼(用的BRVAH,好東西沒必要多說)ide
mAdapter.setOnItemChildClickListener { adapter, view, position -> when (view.id) { R.id.tv_like -> doLike(position) R.id.tv_comment -> doComment(position) R.id.tv_block -> doBlock(position) } } 複製代碼
下面是各個方法的代碼以及onActivityResult代碼oop
//須要登陸才能點贊 private fun doLike(position: Int) { if (!LoginManager.isLogin) { action = "like" clickedPosition = position val i = Intent(this, InputAccountActivity::class.java) startActivityForResult(i, REQUEST_CODE_LOGIN) return } val user = mAdapter.getItem(position) toast("點讚了 ${user?.name}") } //須要登陸且經過實名認證才能評論 private fun doComment(position: Int) { if (!LoginManager.isLogin) { action = "comment" clickedPosition = position val i = Intent(this, InputAccountActivity::class.java) startActivityForResult(i, REQUEST_CODE_LOGIN) return } if (!AuthManager.isAuthed) { action = "comment" clickedPosition = position val i = Intent(this, AuthActivity::class.java) startActivityForResult(i, REQUEST_CODE_AUTH) return } val user = mAdapter.getItem(position) toast("評論了 ${user?.name}") } //須要達到vip3才能屏蔽其餘人 private fun doBlock(position: Int) { if(VipManager.vipLevel<3){ action = "block" clickedPosition = position val i = Intent(this,BuyVipActivity::class.java) startActivityForResult(i,REQUEST_CODE_VIP) return } val user = mAdapter.getItem(position) toast("屏蔽了 ${user?.name}") } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (resultCode == Activity.RESULT_OK) { if (requestCode == REQUEST_CODE_LOGIN) { val loginResult = data?.getBooleanExtra("loginResult", false) if (loginResult == true) { when (action) { "like" -> doLike(clickedPosition) "comment" -> doComment(clickedPosition) "block" -> doBlock(clickedPosition) } } } else if (requestCode == REQUEST_CODE_AUTH) { val authResult = data?.getBooleanExtra("authResult",false) if(authResult == true){ when(action){ "comment" -> doComment(clickedPosition) } } } else if (requestCode == REQUEST_CODE_VIP) { val vipLevel:Int = data?.getIntExtra("vipLevel",0) ?:0 if(vipLevel>=3){ when(action){ "block" -> doBlock(clickedPosition) } } } } } 複製代碼
以點贊爲例,在方法裏先判斷登陸狀態,沒登陸則記錄如下點擊的操做action,以及點擊的位置clickedPosition,並startActivityForResult跳至InputAccountActivity,等待在onActivityResult裏處理登陸結果。若是是已登陸狀態則直接執行點贊操做(這裏用toast代替)。
再來看onActivityResult裏的邏輯,先判斷resultCode,再根據不一樣的requestCode從data裏取出對應的結果值,若是是true(即登陸成功),再根據以前記錄下的action和clickedPosition執行以前中斷的操做。
屏蔽與點贊邏輯相似,至於評論,不過是又加了個實名認證的判斷,再也不贅述。
能夠看到代碼幾乎不存在多少難度,主要就是一層又一層的判斷太繁瑣,致使代碼臃腫。而形成這一切的緣由是全部的處理必須在onActivityResult中進行,若是獲取的結果能直接在doLike中拿到就不會這樣了,也就沒必要再記錄action和clickedPosition了。因此,兇手只有一個,onActivityResult!下面方式二咱們會對此進行處理。
針對方式一的問題,是時候介紹一下我以前寫的一個東西了AvoidOnResult,若是想了解原理能夠看這篇文章如何避免使用onActivityResult,以提升代碼可讀性,若是暫時只想知道怎麼用,我來簡單介紹一下,它主要用來解決startActivityForResult和onActivityResult這種發起調用和回調分離的問題,它的使用方式以下
AvoidOnResult(activity).startForResult(XXXActivity::class.java,object :AvoidOnResult.Callback{
override fun onActivityResult(resultCode: Int, data: Intent?) {
//TODO
}
})
複製代碼
構造器裏須要傳一個activity實例,startForResult方法傳要跳轉過去的class(或intent),同時,再傳一個AvoidOnResult.Callback,在callback的onActivityResult中就能夠處理收到的結果了,而徹底不用重寫activity的onActivityResult方法。
這樣咱們的點擊事件能夠寫成這樣了
when (view.id) { R.id.tv_like -> { if (LoginManager.isLogin) { doLike(position) } else { AvoidOnResult(activity).startForResult(InputAccountActivity::class.java, object : AvoidOnResult.Callback { override fun onActivityResult(resultCode: Int, data: Intent?) { if (resultCode == Activity.RESULT_OK && data?.getBooleanExtra("loginResult", false) == true) { doLike(position) } } }) } } …… } 複製代碼
能夠看到全部的處理都在點擊事件這兒進行了,不須要在onActivityResult統一判斷處理了,也不須要記錄action和clickedPosition。固然咱們還能夠進一步封裝一下,LoginManager代碼以下
object LoginManager { var isLogin = false fun toLogin(activity:Activity,loginCallback: LoginCallback) { AvoidOnResult(activity).startForResult(InputAccountActivity::class.java,object :AvoidOnResult.Callback{ override fun onActivityResult(resultCode: Int, data: Intent?) { if(resultCode == Activity.RESULT_OK && data?.getBooleanExtra("loginResult",false)==true){ loginCallback.onLoginResult(true) }else{ loginCallback.onLoginResult(false) } } }) } interface LoginCallback{ fun onLoginResult(loginResult: Boolean) } } 複製代碼
其餘兩個模塊也相似,最終點擊事件是下面這樣的,而doLike、doComment、doBlock中再也不進行判斷了,能夠看到代碼整齊多了。
mAdapter.setOnItemChildClickListener { adapter, view, position -> when (view.id) { R.id.tv_like -> { if (LoginManager.isLogin) { doLike(position) } else { LoginManager.toLogin(this, object : LoginManager.LoginCallback { override fun onLoginResult(loginResult: Boolean) { if (loginResult) { doLike(position) } } }) } } R.id.tv_comment -> { if(AuthManager.isAuthed){ doComment(position) }else{ AuthManager.toAuth2(this,object :AuthManager.AuthCallback{ override fun onAuthResult(authResult: Boolean) { if(authResult){ doComment(position) } } }) } } R.id.tv_block -> { if (VipManager.vipLevel >= 3) { doBlock(position) } else { VipManager.toBuyVip(this, object : VipManager.VipCallback { override fun onBuyVip(vipLevel: Int) { if (vipLevel >= 3) { doBlock(position) } } }) } } } } 複製代碼
可能有人會問,doComment只進行了實名認證狀態的判斷,沒判斷登陸啊,那是由於在AuthManager的toAuth2方法中處理過登陸的檢查了,也就是說要進行實名認證,首先你得登陸,不然你都進不了實名認證模塊,這個聽起來很合理吧(但後面咱們會推翻它……)
//方式2 fun toAuth2(activity: Activity, authCallback: AuthCallback){ if(LoginManager.isLogin){ realToAuth(activity,authCallback) }else{ LoginManager.toLogin(activity,object :LoginManager.LoginCallback{ override fun onLoginResult(loginResult: Boolean) { if(loginResult){ realToAuth(activity,authCallback) }else{ authCallback.onAuthResult(false) } } }) } } private fun realToAuth(activity: Activity, authCallback: AuthCallback){ AvoidOnResult(activity).startForResult(AuthActivity::class.java,object :AvoidOnResult.Callback{ override fun onActivityResult(resultCode: Int, data: Intent?) { if(resultCode == Activity.RESULT_OK && data?.getBooleanExtra("authResult",false) == true){ authCallback.onAuthResult(true) }else{ authCallback.onAuthResult(false) } } }) } 複製代碼
其實到這裏,相比方式一的代碼已經改善不少了。可是,我說可是,做爲一名程序猿軟件工程師——這個世界上最接近魔法師的神奇職業之一,怎麼能就此知足!繼續優化!
此次要祭出的是AOP(面向切面)了,項目用的是aspectjx,爲Android處理過的aspectj。沒聽過的能夠看我以前寫的文章
AOP:利用Aspectj注入代碼,無侵入實現各類功能,好比一個註解請求權限
若是你仍是不想看的話,我簡單介紹一下(我儘可能說明白吧)
不一樣於面向對象,面向切面編程是針對知足某些條件的切面進行統一處理,比方咱們如今有一個麪包(面向對象裏的對象),須要把它作成漢堡,所須要的操做就是把它中間切一刀(這就是切面了),而後向切面裏塞入一些肉和菜什麼的。
對應如今的例子呢,全部須要驗證登陸的地方就是一個切面,咱們要作的就是肯定這個切面,而後在這個切面統一處理(用@Around,能夠實現方法的攔截或容許繼續執行),判斷登陸狀態,已登陸就容許執行(joinpint.proceed()),不然就跳轉至登陸模塊,登陸成功再執行。
aspectj還涉及到一些Aspect、Pointcut、Advice等名詞,仍是建議瞭解一下 再繼續往下看,我在以後的講述中也會假定各位對此有了一些瞭解。
仍是以點贊登陸爲例,先分析一下,首先咱們要找到須要檢查登陸的切面,而後在Advice中判斷若是已登陸,就容許方法執行,即proceed,這個很容易;若是未登陸,則要走登陸流程,登陸成功再proceed,登陸失敗就至關於攔截了,回顧以前的LoginManager,它須要一個Activity參數,只要能拿到activity實例就行,最簡單粗暴的,我無論你方法在哪,我直接取top Activity,即當前resume的activity,關於top Activity的獲取很少說,我是Application中獲取的,Weak是我本身寫的一個委託,你只把它當弱引用就好了。好了,關於activity實例的獲取解決了,那就正式開始吧
class MyApplication: Application() { companion object { var topActivity by Weak<Activity>() } override fun onCreate() { super.onCreate() registerActivityLifecycleCallbacks(object :ActivityLifecycleCallbacks{ override fun onActivityPaused(activity: Activity?) { } override fun onActivityResumed(activity: Activity?) { topActivity = activity } override fun onActivityStarted(activity: Activity?) { } override fun onActivityDestroyed(activity: Activity?) { } override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) { } override fun onActivityStopped(activity: Activity?) { } override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) { } }) } } 複製代碼
首先是須要登陸的切面,我是經過註解來肯定的,先來個RequireLogin註解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireLogin { boolean proceed() default true; } 複製代碼
能夠看到我定義了一個boolean類型的參數proceed(字面義:繼續,尤指打斷後),由於產品的需求中可能會要求某些地方登陸完後自動繼續以前打斷的操做,有些地方卻不用,這個參數咱們會用來判斷要不要繼續被打斷的操做,即要不要執行joinpoint.proceed()。
繼續,上aspect,首先來個pointcut,全部方法的執行
//全部方法的execution @Pointcut("execution(* *..*.*(..))") public void anyExecution() { } 複製代碼
針對RequireLogin註解,再來個pointcut
//註解有RequireLogin @Pointcut("@annotation(requireLogin)") public void annotatedWithRequireLogin(RequireLogin requireLogin) { } 複製代碼
再來是它們兩個pointcut的交集,也就是全部註解有RequireLogin的方法的執行,這就是須要驗證登陸的切面了
@Pointcut("anyExecution() && annotatedWithRequireLogin(requireLogin)") public void requireLoginPointcut(RequireLogin requireLogin) { } 複製代碼
怎麼攔截處理呢?上Advice
@Around("requireLoginPointcut(requireLogin)") public void requireLogin(final ProceedingJoinPoint proceedingJoinPoint, RequireLogin requireLogin) throws Throwable { final boolean proceed = requireLogin.proceed(); if (LoginManager.INSTANCE.isLogin()) { proceedingJoinPoint.proceed(); } else { Activity activity = MyApplication.Companion.getTopActivity(); if (activity != null) { LoginManager.INSTANCE.toLogin(activity, new LoginManager.LoginCallback() { @Override public void onLoginResult(boolean loginResult) { if (loginResult && proceed) { try { proceedingJoinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } } } }); } } } 複製代碼
proceedingJoinPoint.proceed()就是執行了被攔截的方法,不調用這個方法就至關於代碼被攔截不執行了。邏輯和咱們以前說的同樣,先取了註解的參數proceed,而後判斷登陸狀態,若是登陸了就proceedingJoinPoint.proceed(),不然就先獲取topActivity,走登陸流程,在登陸結果中判斷,若是登陸成功而且註解的參數proceed傳的true,就proceedingJoinPoint.proceed()。
另兩個模塊不贅述了,直接看代碼吧,這時候UserListActivity3的方法是這樣的
//須要登陸才能點贊 @RequireLogin(proceed = true) private fun doLike(position: Int) { val user = mAdapter.getItem(position) toast("點讚了 ${user?.name}") } //須要登陸且經過實名認證才能評論 @RequireAuth(proceed = true) private fun doComment(position: Int) { val user = mAdapter.getItem(position) toast("評論了 ${user?.name}") } //須要達到vip3才能屏蔽其餘人 @RequireVip(proceed = true,requireLevel = 3) private fun doBlock(position: Int) { val user = mAdapter.getItem(position) toast("屏蔽了 ${user?.name}") } 複製代碼
代碼中沒有一丁點的判斷,doLike須要登陸,那就給它加個RequireLogin註解,要求登陸完自動執行點贊,那就給proceed參數設爲true,不然就false。之後其餘地方若是也要求登陸了,只須要在對應的方法上也給它加個註解,哪裏登陸注哪裏。
仍是doComment,明明又要登陸又要註冊,但是卻只有一個RequireAuth註解,爲何,由於AuthManager的toAuth上加了RequireLogin註解啊,即要想實名認證,首先你得登陸。
//方式3請添加註解,方式四請注掉下面的註解 @RequireLogin(proceed = true) fun toAuth(activity: Activity, authCallback: AuthCallback){ AvoidOnResult(activity).startForResult(AuthActivity::class.java,object :AvoidOnResult.Callback{ override fun onActivityResult(resultCode: Int, data: Intent?) { if(resultCode == Activity.RESULT_OK && data?.getBooleanExtra("authResult",false) == true){ authCallback.onAuthResult(true) }else{ authCallback.onAuthResult(false) } } }) } 複製代碼
到這裏代碼夠精簡了吧,一個註解就能完成以前一大堆的判斷,難道還能再減小?下一步的優化再也不是精簡代碼了,而是增長靈活性。以前兩次提到了實名認證對於登陸的依賴,即要想進行實名認證的話,首先得檢查登陸。依賴關係大概這樣的 doComment -> toAuth -> toLogin,大概是個鏈式的依賴關係,雖然這個需求聽起來很合理,可是代碼這麼寫卻缺乏靈活性。怎麼說呢?好比如今又有需求了,想屏蔽別人首先你得是vip3,而後還要經過了實名認證,不少人第一時間想到的多是像實名認證那樣,我在VipManager的toBuyVip方法上再加個RequireAuth註解,然而並不能夠,由於要求不登陸也能夠跳轉到vip購買頁面,在點購買的時候纔要求登陸,而若是給toBuyVip添加RequireAuth註解以後依賴關係就是這樣的了doBlock -> toBuyVip -> toAuth -> toLogin,這樣在點屏蔽的時候會先走登陸註冊流程,致使用戶沒法以遊客的身份進入vip購買頁面。固然咱們在此不討論需求的合理性,不討論遊客該不應進入vip購買頁面。
說這麼多也不知道表達清楚沒,我就是想說,咱們如今存在的問題是各個切面有依賴關係,有耦合,若是讓它們彼此獨立,咱們能夠自由地組合就行了,好比實名認證模塊就只管判斷實名認證的狀態,無論你登沒登陸(你沒登陸,那實名認證就該是false的狀態)。咱們往方法上加註解的時候直接加多個註解,好比doComment,要求兩點,一登陸,二認證,那我就註解RequireLogin和RequireAuth;doBlock要求vip3和實名認證,那我就註解RequireVip和RequireAuth,就像搭積木同樣,自由組合。
此次以doComment爲例,既然要作到各個切面獨立,那就先把AuthManager.toAuth方法的RequireLogin註解去掉,讓它不依賴於登陸切面,而後咱們往doComment上加兩個註解,RequireLogin、RequireAuth,而後運行一下先看下效果,點擊評論,發現兩個問題:
首先先說第二個問題,咱們的幾種方式本質上都是用的onActivityResult,AvoidOnResult的callback就是在構造器傳入的activity實例的onActivityResult中調用的,若是這個activity實例finish掉了,那callback就不會調用了。咱們能夠打個斷點,分別打在AuthAspect和LoginAspect中獲取topActivity的地方,點擊評論首先會走到AuthAspect中,這裏咱們能夠看到獲取到的topActivity是UserListActivity4,是咱們指望的結果,沒問題。放開斷點繼續走,走完實名認證的流程後會進入LoginAspect的代碼,這裏咱們發現獲取到的topActivity是AuthActivity,也就是這時雖然已經在UserListActivity4的onActivityResult代碼中了,可是當前resume狀態的activity卻仍是AuthActivity,而咱們再經過AuthActivity去startForResult,callback確定不會執行了,由於它立刻就要finish掉了,關於onActivityResult和onResume的執行順序問題在Activity的onActivityResult的源碼註釋中其實說的也很明白了,onActivityResult比onResume先執行。
You will receive this call immediately before onResume() when your activity is re-starting.
那也就是說在多個切面的狀況下,咱們直接獲取resume的Activity是不可行的。那怎麼解決呢?個人方法是先經過joinpoint的this,target,args,看看這些地方有沒有能拿到的activity,若是有,就用這裏拿到的activity,若是沒有再用top Activity,下面的方法只判斷了Activity,其實若是能獲取到Fragment或其餘什麼類型的實例,再間接獲取到Activity也能夠,這裏圖簡單沒作太多處理。
public class AspectUtils { public static Activity getActivity(JoinPoint joinPoint){ //先看this if(joinPoint.getThis() instanceof Activity){ return (Activity) joinPoint.getThis(); } //target if(joinPoint.getTarget() instanceof Activity){ return (Activity) joinPoint.getTarget(); } //args for(Object arg:joinPoint.getArgs()){ if (arg instanceof Activity){ return (Activity) arg; } } //若是實在找不到,再返回topActivity return MyApplication.Companion.getTopActivity(); } } 複製代碼
第二個問題解決了再來看第一個問題,aspectj織入順序問題,我是在這裏找到方法的https://stackoverflow.com/questions/11850160/aspectj-execution-order-precedence-for-multiple-advice-within-one-aspect
When two pieces of advice defined in the same aspect both need to run at the same join point, the ordering is undefined (since there is no way to retrieve the declaration order via reflection for javac-compiled classes). Consider collapsing such advice methods into one advice method per joinpoint in each aspect class, or refactor the pieces of advice into separate aspect classes - which can be ordered at the aspect level.
也就是說同一個aspect中的多個advice的順序是不肯定的,能夠考慮把想排序的advice分別放到不一樣的aspect中,而後對這些aspect排序(用@DeclarePrecedence)
分拆aspect很簡單,沒必要說。排序再建立個類,以下
@Aspect @DeclarePrecedence("LoginAspect,VipAspect,AuthAspect") public class CoordinationAspect { // empty } 複製代碼
評論的時候要求先登陸,再實名認證,因此LoginAspect在AuthAspect前面,屏蔽的時候要求首先是vip,而後還要經過了實名認證,因此VipAspect在AuthAspect前面。
這樣以後再運行一下,應該已經沒問題了。
方式四的代碼以下,相比方式三更靈活,能夠自由組合,固然要肯定好各個切面的順序
//須要登陸才能點贊 @RequireLogin(proceed = true) private fun doLike(position: Int) { val user = mAdapter.getItem(position) toast("點讚了 ${user?.name}") } //須要登陸且經過實名認證才能評論 @RequireLogin(proceed = true) @RequireAuth(proceed = true) private fun doComment(position: Int) { val user = mAdapter.getItem(position) toast("評論了 ${user?.name}") } //須要達到vip3且經過實名認證才能屏蔽其餘人 @RequireVip(proceed = true,requireLevel = 3) @RequireAuth(proceed = true) private fun doBlock(position: Int) { val user = mAdapter.getItem(position) toast("屏蔽了 ${user?.name}") } 複製代碼
demo裏還有個ShowConfirmAspect,是用來在一個方法執行前對用戶進行一些詢問操做,彈個對話框,根據用戶反饋決定要不要執行該方法,我在MainActivity的onBackPressed上加了該註解來詢問用戶是否要退出應用,這裏不細說了,只是想告訴你們面向切面有不少應用場景,特別適合進行一些統一的處理,從而避免大量的重複工做,千萬不要侷限於本文所舉的幾個例子中。