安卓元件化應用的6.0許可權適配優化方案

weixin_34185560發表於2017-08-10

元件化應用

元件化應用的概念最近挺火的。隨著app版本的迭代,業務也會變的越來越複雜。元件化應用能將每個業務都單獨分成一個模組,作為一個元件(Module),業務模組彼此互不依賴,然後讓這些業務模組都依賴公共模組(也是Module)等,用路由的方式替代startactivity進行模組間的跳轉和資料傳遞。這就是元件化應用的簡單概念。

1760489-b43283d0883a3283.jpg
u=3895375886,3606044301&fm=11&gp=0.jpg

因為module與module之間是程式碼隔離的,互不依賴,所以新增或移除module是很方便的,也方便了應用的多人並行開發。

Android6.0許可權

Android6.0已經出來快兩年了,除了繼續推進Material Design,相信最直觀的改變就是許可權申請方式了:許可權模式從一開始的全部列出授予,變成了現在的執行時動態申請。下圖列出了截止7月份最新的系統佔有率。雖然碎片化問題依舊嚴重,但6.0以上系統佔有率也已經接近一半了。所以說各位安卓開發者們,如果你的應用還沒適配6.0的話,可要抓緊了。

1760489-25b4885798837b17.png
QQ截圖20170810160140.png

如何適配

簡單介紹了上面的兩個概念後,我們們切回正題。因為最近在做一個APP的元件化改造,原來的許可權適配方案是跳到一個空白activity做申請然後回撥申請結果的,但是現在module與module之間是互不依賴的,所以activity之間介面回撥的方式是行不通的。一時間不知道有什麼好的方法,也參考了github上幾個主流的許可權適配庫,但遺憾都沒有對元件化應用提供一個專門的解決方案。
因為這個專案採用的路由是阿里的ARouter。查閱文件後發現裡面有攔截器的功能,下面是簡單的示例程式碼。

// 比較經典的應用就是在跳轉過程中處理登陸事件,這樣就不需要在目標頁重複做登陸檢查
// 攔截器會在跳轉之間執行,多個攔截器會按優先順序順序依次執行
  @Interceptor(priority = 8, name = "測試用攔截器")
public class TestInterceptor implements IInterceptor {
    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {
    
    callback.onContinue(postcard);  // 處理完成,交還控制權
    // callback.onInterrupt(new RuntimeException("我覺得有點異常"));      // 覺得有問題,中斷路由流程

    // 以上兩種至少需要呼叫其中一種,否則不會繼續路由
    }

    @Override
    public void init(Context context) {
    // 攔截器的初始化,會在sdk初始化的時候呼叫該方法,僅會呼叫一次
    }
}

那麼我們何不利用這個攔截器,在跳到一個需要頁面(比如相機頁面)之前 進行統一攔截,然後判斷許可權是否擁有,如果後則callback.onContinue(postcard),繼續跳轉,否則callback.onInterrupt(null),攔截跳轉並執行許可權申請。
同時通常一個應用通常有必要許可權,沒有必要許可權應用無法正常執行,如Manifest.permission.READ_PHONE_STATE,還有非必要許可權,如Manifest.permission.ACCESS_FINE_LOCATION
那麼我們可以用priority欄位建立優先順序最高的攔截器,檢測所有跳轉時是否有必要許可權,否則先申請必要許可權,然後繼續觸發下一個優先順序低的攔截器攔截檢測普通許可權。

實際實現

定義一個permissionActivity,讓所有需要跳轉許可權的頁面的activity繼承這個,因為如上文講的,我們實際許可權的申請和處理不是在真正需要許可權的頁面完成,而是在上一個頁面申請完成再跳到需要許可權的頁面的。如果頁面太多建議所有頁面都繼承這個activity。

public class PermissionActivity extends FragmentActivity {
  private PermissionListener permissionListener;

    public void setPermissionListener(PermissionListener permissionListener) {
        this.permissionListener = permissionListener;
    }
    /**
     * 許可權請求結果
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (permissionListener != null ) {
            permissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }

建立攔許可權截器:

@Interceptor(priority = 1)
public class PermissionInterceptor implements IInterceptor {

    @Override
    public void process(final Postcard postcard, final InterceptorCallback callback) {
        final Activity activity = ActivityHelper.last();
        final  String[] PERMISSIONS = new String[]{
            Manifest.permission.READ_EXTERNAL_STORAGE,
    };
        if (activity != null && !PermissionHelper.hasPermission(activity)) {
            permissionRequest(postcard, callback, (PermissionActivity) activity, permissions);
        } else {
            callback.onContinue(postcard);  // 已有許可權,無需申請,繼續跳轉
        }
    }

    private void permissionRequest(final Postcard postcard, final InterceptorCallback interceptorCallback, final PermissionActivity activity, final String[] permissions) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                PermissionHelper.requestPermissions(activity, permissions, new PermissionListener() {
                    @Override
                    public void onsuccessed() {
                        callback.onContinue(postcard);// 許可權申請成功,繼續跳轉
                    }

                    @Override
                    public boolean onfail() {
                        callback.onInterrupt(null);// 許可權申請失敗,攔截跳轉
                    }
                });
            }
        };
        MainLooper.runOnUiThread(runnable );
    }

    @Override
    public void init(Context context) {
    
    }
}

實際實現中有幾個注意點:
1.攔截器init(Context context)方法中的context不是當前頁的activity,而是application,也就是說你在攔截器裡是無法直接拿到當前頁的上下文的。所以你需要額外維護一個activity棧,在每個acitivity的oncreate()方法中入棧,在ondestory()中出棧,然後在攔截器裡面取出棧頂的acitivity,也就是當前頁的acitivity。
2.void process(Postcard postcard, InterceptorCallback callback)方法在子執行緒中執行,如果要執行許可權申請需要先切換回主執行緒。

public class MainLooper extends Handler {
    private static MainLooper instance = new MainLooper(Looper.getMainLooper());

    protected MainLooper(Looper looper) {
        super(looper);
    }

    public static MainLooper getInstance() {
        return instance;
    }

    public static void runOnUiThread(Runnable runnable) {
        if(Looper.getMainLooper().equals(Looper.myLooper())) {
            runnable.run();
        } else {
            instance.post(runnable);
        }

    }
}

總結

這樣實現的好處也是顯而易見的:所有的許可權申請操作都在攔截器裡完成,對原始碼無入侵:所有的許可權申請彈框都出現在需要許可權的頁面上一個activity。我們只要讓這個activity繼承permissionActivity並處理onRequestPermissionsResult的回撥即可,其他譬如處理許可權申請,處理許可權申請成功,失敗操作都統一在攔截器裡完成即可。

相關文章