談談App的統一跳轉和ARouter

菜刀文發表於2017-10-25

App中每次頁面跳轉,都需要呼叫統一導航, 它用的非常頻繁, 有必要對它進行一下梳理. 讓他能用起來簡單方便, 同時能支援各種常用的跳轉業務場景.

一. Android跳轉遇到的問題

1.intent-filter跳轉不好管理

 Intent intent = new Intent();  
 intent.setAction(Intent.ACTION_SENDTO);  
 intent.setData(Uri.parse("smsto:10086"));  
 context.startActivity(intent);  

如果專案分多個Module, Activity需要在各自Module的AndroidManifest.xml中宣告,容易重複,不好統一管理.

2.Activity class跳轉耦合性高

//通過設定目標class跳轉 
Intent intent = new Intent();  
intent.setClass(context,TargetActivity.class);  
context.startActivity(intent);  

A如果要跳轉到TargetActivity, A要引用到TargetActivity. 造成:

  1. 如果專案多個Module開發,底層module不能跳轉到高層Activity
  1. 如果TargetActivity類名變化, 對應呼叫方都需要改動

3. 混合開發時,H5/Weex跳轉新介面不方便

內建H5要跳轉 Native頁面, 通過JsBidge把目標資訊傳過來.

兩種方式:

方式1: 直接提供目標Activity的 Action 跳過去.
方式2: Native維護一個<描述,Activity資訊>的Map, H5傳過來Activiy的”描述”, Native在Map中查到後,進行跳轉.

方式1的問題:

一般H5會同時在”Android/ios”容器中, 所以最好的實踐是:H5做跳轉時不需要區分平臺和版本. 如果利用Action跳轉,

1)Action命名要符合兩個平臺的規範

2)如果Native不支援目標Action,還需要做跳轉失敗後處理.

方式2的問題:

1)維護<描述,Activity資訊>的列表麻煩事,需要單獨角色管理.

2)同樣存的”Activity資訊”也有問題1,2中提到的問題

都有的問題:

處理跳轉的Bridge類,可能拿不到context,這需要拿Application的Context,大家都判斷略嫌麻煩.

4.跳轉到”未知頁面”的統一處理

比如2.0版本新加了”訊息”功能,App1.0版本沒有.
此時1.0版本的App中,”H5/push” 嘗試開啟”訊息”頁面, 肯定是不支援的. 這時候有幾種策略:

  1. H5/Push能判斷Native支援頁面的能力,如果不支援,就不呼叫
  2. Native收到呼叫未知頁面, 不做任何動作.
  3. Native收到呼叫未知頁面, 提示這是新版功能,建議更新版本.

5. 業務降級/重定向

  1. 比如A/B測試:
    Native可以根據配置, 跳轉不同的實現頁面
  2. 業務降級:
    某個業務本來Native實現, 降級為H5實現, 這時候跳轉時切換到H5頁面.

6.統一加參

跳轉到目標頁面前,能統一加引數.
實現比如打點, 新增通用引數操作.

7.外部呼叫的統一入口

考慮這種業務場景: App有 A,B,C三個頁面, 提供給外部呼叫.
這時候一般兩種實現方式:

方式1: A,B,C的Activity 在AndroidManifest.xml中export=true,並且設定 intent-filter
方式2: App設定一個統一的Router-Activity, 外部跳轉到A,B,C 都統一先統一到Router-Activity, 他在拉起A,B,C

方式1分析:

除非真的提供通用的功能(拍照/圖片處理/..)給外部呼叫, 否則export一個Activity是不必要也不安全的. 為了安全,App不會export大量的Activity. 這意味著通過這種機制, 外部能呼叫內部的功能較少.

方式2分析:

優點:

  1. 只暴露了一個Router-Activity. 安全和好管理.
  2. Router-Activity裡面可以做一些呼叫者的安全校驗, 如果校驗通過可以執行跳轉App的全部頁面. 這樣給能外部呼叫app更多頁面的機會, 也兼顧了安全.

缺點:
外部跳轉需要一個Activity中轉一下,直觀上感覺效率低一些. 但是實際感覺基本沒有影響.

二. 明確需求

根據問題和業務場景, 我們的”統一跳轉”的需求也基本明確:

  1. “宣告/使用” 簡單.
  1. 適用多module開發,避免直接依賴.
  2. 統一協議, 適用”H5/Weex/Native” 跳轉 “Native”, 對”Android/ios”兩個平臺協議應該是一樣的.
  3. 有統一的外部呼叫入口
  4. 能對”不支援”的跳轉統一處理
  5. 支援跳轉前預處理
  6. 支援重定向

三.解決方案ARouter

ARouterARouter-github 很好的解決了上述問題.
下面是他的對應的方案.

1.使用簡單:

  1. 每個Activity在類中自宣告,”程式碼-路徑”對應一目瞭然
@Route(path = "/test/activity")
public class YourActivity extend Activity {
    ...
}
  1. 跳轉新頁面簡單,不需要知道目標ActivityContext,intent-filter,目標的Activity
ARouter.getInstance().build(path).with(bundle).navigation();   

2.頁面利於統一管理

所有頁面可以統一定義. 一目瞭然

String PAGE_MAIN = "/navigateTo/main";
String PAGE_H5 = "/navigateTo/h5";
String PAGE_WEEX = "/navigateTo/weex";
...

3.便於設定統一Activity承載外部跳轉

public class SchameFilterActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    //安全/版本校驗
    ....
    
    Uri uri = getIntent().getData();
    ARouter.getInstance().build(uri).navigation();
    finish();
    }
}

4.處理”未知頁面”的跳轉結果

 ARouter.getInstance().build("/test/1").navigation(this, new NavigationCallback() {
    @Override
    public void onFound(Postcard postcard) {
      ...
    }
    @Override
    public void onLost(Postcard postcard) {
        //可以處理,提示升級版本之類
    }
});

5. 自定義全域性降級策略

// 實現DegradeService介面,並加上一個Path內容任意的註解即可
@Route(path = "/xxx/xxx")
public class DegradeServiceImpl implements DegradeService {
  @Override
  public void onLost(Context context, Postcard postcard) {
    // do something.
  }

  @Override
  public void init(Context context) {

  }
}

6. 重寫跳轉URL實現重定向

// 實現PathReplaceService介面,並加上一個Path內容任意的註解即可
@Route(path = "/xxx/xxx") // 必須標明註解
public class PathReplaceServiceImpl implements PathReplaceService {
    /**
     * For normal path.
     *
     * @param path raw path
     */
    String forString(String path) {
    return path;    // 按照一定的規則處理之後返回處理後的結果
    }

   /**
    * For uri type.
    *
    * @param uri raw uri
    */
   Uri forUri(Uri uri) {
    return url;    // 按照一定的規則處理之後返回處理後的結果
   }
}

技術分析

1.建立 Url-Activity 的對應關係

ARouter最後是通過下面方式跳轉的.

//_ARouter.java  
Intent intent = new Intent(currentContext,postcard.getDestination());
intent.putExtras(postcard.getExtras());

所以要AROUTER需要維護一個 Path和Activity class的對應關係.
他利用

  1. javapoet 在編譯時候生成類資訊
  2. 初始化時,收集主創Path/Activity資訊. 所有資訊存在WareHouse中.

screenshot.png

2. 跳轉流程

screenshot.png

其他技術

1. 屬性設定在gradle.properties中

BUILDTOOLS_VERSION=25.0.0

使用:

compile "com.android.support:support-v4:${SUPPORT_LIB_VERSION}"   
buildToolsVersion BUILDTOOLS_VERSION

2.TreeMap

HashMap通過hashcode對其內容進行快速查詢,而 TreeMap中所有的元素都保持著某種固定的順序,如果你需要得到一個有序的結果你就應該使用TreeMap(HashMap中元素的排列順序是不固定的)

3.Instrumentation的使用

你可以將Instrumentation理解為一種沒有圖形介面
的,具有啟動能力的,用於監控其他類(用Target
Package宣告)的工具類。任何想成為Instrumentation的類必須繼承android.app.Instrumentation。

下面是這個類的解釋:

“Base class for implementing application instrumentation code. When running with instrumentation turned on, this class will be instantiated for you before any of the application code, allowing you to monitor all of the interaction the system has with the application. An Instrumentation implementation is described to the system through an AndroidManifest.xml`s tag.“

4.volatile

Java多執行緒/併發09、淺談volatile

  1. volatile重要工作是避免執行緒髒讀:當執行緒對volatile變數進行讀操作時,會先將2. 自己工作記憶體中的變數置為無效,之後再通過主記憶體拷貝新值到工作記憶體中使用。
  2. volatile解決的是變數在多個執行緒之間的可見性,但不能完全保證資料的原子性。
    現在JVM經過優化,已不會出現liveness failure 。所以沒事別用volatile。

5. CountDownLatch

CountDownLatch的一個非常典型的應用場景是:有一個任務想要往下執行,但必須要等到其他的任務執行完畢後才可以繼續往下執行。假如我們這個想要繼續往下執行的任務呼叫一個CountDownLatch物件的await()方法,其他的任務執行完自己的任務後呼叫同一個CountDownLatch物件上的countDown()方法,這個呼叫await()方法的任務將一直阻塞等待,直到這個CountDownLatch物件的計數值減到0為止。

6.獲取CPU個數

CPU_COUNT = Runtime.getRuntime().availableProcessors()

7.捕捉執行緒異常

// 捕獲多執行緒處理中的異常
thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
    @Override
    public void uncaughtException(Thread thread, Throwable ex) {
        ARouter.logger.info(Consts.TAG, "Running task appeared exception! Thread [" + thread.getName() + "], because [" + ex.getMessage() + "]");
       }
   }); 

8.javapoet

build  classpath `com.neenbedankt.gradle.plugins:android-apt:1.4` 

使用 annotationProcessor
dependencies {
    annotationProcessor project(`:arouter-compiler`)
}

9.Activity啟動

int flags = postcard.getFlags();
    if (-1 != flags) {
      intent.setFlags(flags);
    } else if (!(currentContext instanceof Activity)) {     
    // Non activity, need less one flag.
      intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
} 

10. 訪問者模式

//設定WareHouse

public interface IRouteGroup {
    /**
     * Fill the atlas with routes in group.
     */
    void loadInto(Map<String, RouteMeta> atlas);
}

iGroupInstance.loadInto(Warehouse.routes);


相關文章