Android7.0適配總結

一、權限更改

對於面向 Android 7.0 的應用,Android 框架執行的 StrictMode API 政策禁止在您的應用外部公開 file:// URI。如果一項包含文件 URI 的 intent 離開您的應用,則應用出現故障,並出現 FileUriExposedException 異常。

要在應用間共享文件,您應發送一項 content:// URI,並授予 URI 臨時訪問權限。也就是說,對於應用間共享文件這塊,Android N中做了強制性要求

來看一段代碼

String cachePath = getApplicationContext().getExternalCacheDir().getPath();
File picFile = new File(cachePath, "test.jpg");
Uri picUri = Uri.fromFile(picFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, picUri);
startActivityForResult(intent, 100);

這是常見的打開系統相機拍照的代碼,拍照成功後,照片會存儲在picFile文件中。

這段代碼在Android 7.0之前是沒有任何問題,但是如果你嘗試在7.0的系統上運行,會拋出FileUriExposedException異常。

使用FileProvider

FileProvider使用大概分爲以下幾個步驟:

  1. manifest中申明FileProvider
  2. res/xml中定義對外暴露的文件夾路徑
  3. 生成content://類型的Uri
  4. 給Uri授予臨時權限
  5. 使用Intent傳遞Uri
1.manifest中申明FileProvider:
<manifest>
  ...
  <application>
    ...
    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="com.demo.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/paths" />
    </provider>
    ...
  </application>
</manifest>

android:name:provider你可以使用v4包提供的FileProvider,或者自定義的,只需要在name申明就好了,一般使用系統的就足夠了。

android:authorities:類似schema,命名空間之類,後面會用到。

android:exported:false表示我們的provider不需要對外開放。

android:grantUriPermissions:申明爲true,你才能獲取臨時共享權限。

2. res/xml中定義對外暴露的文件夾路徑:

新建paths.xml,文件名隨便起,後面會引用到。

<paths xmlns:android="http://schemas.android.com/apk/res/android">
  <files-path name="my_images" path="images"/>
</paths>

name:一個引用字符串。

path:文件夾“相對路徑”,完整路徑取決於當前的標籤類型。

path可以爲空,表示指定目錄下的所有文件、文件夾都可以被共享。

paths這個元素內可以包含以下一個或多個,具體如下:

<files-path name="name" path="path" />

物理路徑相當於Context.getFilesDir() + /path/。

<cache-path name="name" path="path" />

物理路徑相當於Context.getCacheDir() + /path/。

<external-path name="name" path="path" />

物理路徑相當於Environment.getExternalStorageDirectory() + /path/。

<external-files-path name="name" path="path" />

物理路徑相當於Context.getExternalFilesDir(String) + /path/。

<external-cache-path name="name" path="path" />

物理路徑相當於Context.getExternalCacheDir() + /path/。

3.生成content://類型的Uri

我們通常通過File生成Uri的代碼是這樣:

File picFile = xxx;
Uri picUri = Uri.fromFile(picFile);

這樣生成的Uri,路徑格式爲file://xxx。這種Uri是無法在App之間共享的,我們需要生成content://xxx類型的Uri,方法就是通過Context.getUriForFile來實現:

File imagePath = new File(Context.getFilesDir(), "images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), 
                 "com.demo.fileprovider", newFile)
4.給Uri授予臨時權限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
               | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

FLAG_GRANT_READ_URI_PERMISSION:表示讀取權限;
FLAG_GRANT_WRITE_URI_PERMISSION:表示寫入權限。

5.使用Intent傳遞Uri

以開頭的拍照代碼作爲示例,需要這樣改寫:

File imagePath = new File(Context.getFilesDir(), "images");
if (!imagePath.exists()){imagePath.mkdirs();}
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), 
                 "com.mydomain.fileprovider", newFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri);
// 授予目錄臨時共享權限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
               | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivityForResult(intent, 100);   

二、廣播

於Android N後臺的優化主要是關閉了三項系統廣播:網絡狀態變更廣播、拍照廣播以及錄像廣播。

網絡變化的廣播(CONNECTIVITY_CHANGE),當網絡發生變化時所有註冊了隱式監聽網絡變化的app都會被啓動。刪除這些廣播可以顯著提升設備性能和用戶體驗。同樣地,拍照廣播和錄視頻廣播(ACTION_NEW_PICTURE or ACTION_NEW_VIDEO)也會出現上述情況。

在Android N平臺下即使在Manifest.xml清單文件中註冊了 CONNECTIVITY_ACTION廣播,在網絡發生變化時也不會接收到任何的信息。但是正在前臺運行的應用程序如果在主線程中通過Context.registerReceiver()動態註冊了CONNECTIVITY_ACTION廣播,該應用程序仍然可以接收到該廣播。

三、分屏

Android N允許用戶一次在屏幕中使用兩個App,用戶可以左右並排/上下襬放兩個App來使用。用戶還可以左右/上下拖拽中間的那個小白線來改變兩個App的尺寸。
20170504149388867443363.png 20170504149388869827151.png

如何操作來進入分屏模式的:

  1. 點擊右下角的方塊,進入任務管理器,長按一個App的標題欄,將其拖入屏幕的高亮區域,這個App金進入了分屏模式。然後在任務管理器中選擇另一個App,單擊它使得這個App也進入分屏模式。
  2. 打開一個App,然後長按右下角的方塊,此時已經打開的這個App將進入分屏模式。然後在屏幕上的任務管理器中選擇另外一個App,單擊它使得這個App也進入分屏模式。

分屏模式的生命週期

官方說法:在分屏模式下,用戶最近操作、激活過的Activity將被系統視爲topmost。而其他的Activity都屬於paused狀態,即使它是一個對用戶可見的Activity。但是這些可見的處於paused狀態的Activity將比那些不可見的處於paused狀態的Activity得到更高優先級的響應。當用戶在一個可見的paused狀態的Activity上操作時,它將得到恢復resumed狀態,並被系統視爲topmost。而之前那個那個處於topmpst的Activity將變成paused狀態。

那麼這種可見的pause的狀態將帶來什麼影響呢?

在分屏模式中,一個App可以在對用戶可見的狀態下進入paused狀態,所以你的App在處理業務時,應該知道自己什麼時候應該真正的暫停。例如一個視頻播放器,如果進入了分屏模式,就不應該在onPaused()回調中暫停視頻播放,而應該在onStop()回調中才暫停視頻,然後在onStart回調中恢復視頻播放。關於如果知道自己進入了分屏模式,在Android N的Activity類中,增加了一個void onMultiWindowChanged(boolean inMultiWindow)回調,所以我們可以在這個回調知道App是不是進入了分屏模式。

分屏時Activity的生命週期

  • 當前顯示自己的應用頁面,長按多任務鍵時出現分屏

    onConfigurationChanged()-> onMultiWindowModeChanged()-> onPause()-> onStop()-> onDestroy()-> onCreate()-> onStart()-> onResume()-> onPause()

  • 分屏時長按多任務鍵,全屏顯示自己的應用時

    onPause()-> onStop()-> onDestroy()-> onCreate()-> onStart()-> onResume()-> onPause()-> onConfigurationChanged()-> onMultiWindowModeChanged()-> onResume()

如何設置App的分屏模式

怎樣才能讓App進入分屏模式呢?有下面這幾個屬性。

android:resizeableActivity

直接在AndroidManifest.xml中的或者標籤下設置新的屬性android:resizeableActivity=”true”。

設置了這個屬性後,你的App/Activity就可以進入分屏模式了。

如果這個屬性被設爲false,那麼你的App將無法進入分屏模式,如果你在打開這個App時,長按右下角的小方塊,App將仍然處於全屏模式,系統會彈出Toast提示你無法進入分屏模式。這個屬性在你target到Android N後,android:resizeableActivity的默認值就是true。

注意:假如你沒有適配到Android N(targetSDKVersion < Android N),打包App時的compileSDKVersion < Android N,你的App也是可以支持分屏的!!!!原因在於:如果你的App沒有設置 僅允許Activity豎屏/橫屏,即沒有設置android:screenOrientation=”XXX”屬性時,運行Android N系統的設備還是可以將你的App分屏!! 但是這時候系統是不保證運行時的穩定性的,在進入分屏模式時,系統首先也會彈出Toast來提示你說明這個風險。

最新的Android N SDK中,Activity類中增加了下面的方法。

  • inMultiWindow():返回值爲boolean,調用此方法可以知道App是否處於分屏模式。
  • onMultiWindowChanged(boolean inMultiWindow):當Activity進入或者退出分屏模式時,系統會回調這個方法來通知開發者。回調的參數inMultiWindow爲boolean類型,如果inMultiWindow爲true,表示Activity進入分屏模式;如果inMultiWindow爲false,表示退出分屏模式。

支持拖拽

現在可以實現在兩個分屏模式的Activity之間拖動內容。Android N Preview SDK中,View已經增加支持Activity之間拖動的API。具體的類和方法主要用到下面幾個新的接口:

  • View.startDragAndDrop():View.startDrag() 的替代方法,需要傳遞View.DRAG_FLAG_GLOBAL來實現跨Activity拖拽。如果需要將URI權限傳遞給接收方Activity,還可以根據需要設置View.DRAG_FLAG_GLOBAL_URI_READ或者View.DRAG_FLAG_GLOBAL_URI_WRITE。
  • View.cancelDragAndDrop():由拖拽的發起方調用,取消當前進行中的拖拽。
  • View.updateDragShadow():由拖拽的發起方調用,可以給當前進行的拖拽設置陰影。
  • android.view.DropPermissions:接收方App所得到的權限列表。
  • Activity.requestDropPermissions():傳遞URI權限時,需要調用這個方法。傳遞的內容存儲在DragEvent中的ClipData裏。返回值爲前面的android.view.DropPermissions。

20170505149395235143362.png

在FirstActivity中,發起拖拽。

imageView.setOnTouchListener(new View.OnTouchListener() {
    public boolean onTouch(View view, MotionEvent motionEvent) {
        if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
            /**
             *  構造一個ClipData,將需要傳遞的數據放在裏面
             */
            ClipData.Item item = new ClipData.Item((CharSequence) view.getTag());
            String[] mimeTypes = {ClipDescription.MIMETYPE_TEXT_PLAIN};
            ClipData dragData = new ClipData(view.getTag().toString(), mimeTypes, item);
            View.DragShadowBuilder shadow = new View.DragShadowBuilder(imageView);
            /**
             * startDragAndDrop是Android N SDK中的新方法,替代了以前的startDrag,
             * flag需要設置爲DRAG_FLAG_GLOBAL
             */
            view.startDragAndDrop(dragData, shadow, null, View.DRAG_FLAG_GLOBAL);
            return true;
        } else {
            return false;
        }
    }
});

在SecondActivity中,接收這個拖拽的結果,在ACTION_DROP事件中,把結果顯示出來。

dropedText.setOnDragListener(new View.OnDragListener() {
    @Override
    public boolean onDrag(View view, DragEvent dragEvent) {
        switch (dragEvent.getAction()) {
            case DragEvent.ACTION_DRAG_STARTED:
                Log.d(TAG, "Action is DragEvent.ACTION_DRAG_STARTED");
                break;
            case DragEvent.ACTION_DRAG_ENTERED:
                Log.d(TAG, "Action is DragEvent.ACTION_DRAG_ENTERED");
                break;
            case DragEvent.ACTION_DRAG_EXITED:
                Log.d(TAG, "Action is DragEvent.ACTION_DRAG_EXITED");
                break;
            case DragEvent.ACTION_DRAG_LOCATION:
                break;
            case DragEvent.ACTION_DRAG_ENDED:
                Log.d(TAG, "Action is DragEvent.ACTION_DRAG_ENDED");
                break;
            case DragEvent.ACTION_DROP:
                Log.d(TAG, "ACTION_DROP event");
                //在這裏顯示接收到的結果
                dropedText.setText(dragEvent.getClipData().getItemAt(0).getText());
                break;
            default:
                break;
        }
        return true;
    }
});

分屏原理

分屏功能的實現主要依賴於ActivityManagerService與WindowManagerService這兩個系統服務,它們都位於system_server進程中。該進程是Android系統中一個非常重要的系統進程。Framework中的很多服務都位於這個進程中。

整個Android的架構是CS的模型,應用程序是Client,而system_server進程就是對應的Server。

應用程序調用的很多API都會發送到system_server進程中對應的系統服務上進行處理,例如startActivity這個API,最終就是由ActivityManagerService進行處理。

而由於應用程序和system_server在各自獨立的進程中運行,因此對於系統服務的請求需要通過Binder進行進程間通訊(IPC)來完成調用,以及調用結果的返回。

ActivityManagerService負責Activity管理。

對於應用中創建的每一個Activity,在ActivityManagerService中都會有一個與之對應的ActivityRecord,這個ActivityRecord記錄了應用程序中的Activity的狀態。ActivityManagerService會利用這個ActivityRecord作爲標識,對應用程序中的Activity進程調度,例如生命週期的管理。

實際上,ActivityManagerService的職責遠超出的它的名稱,ActivityManagerService負責了所有四大組件(Activity,Service,BroadcastReceiver,ContentProvider)的管理,以及應用程序的進程管理。

WindowManagerService負責Window管理。包括:

窗口的創建和銷燬
窗口的顯示與隱藏
窗口的佈局
窗口的Z-Order管理
焦點的管理
輸入法和壁紙管理
等等 每一個Activity都會有一個自己的窗口,在WindowManagerService中便會有一個與之對應的WindowState。WindowManagerService以此標示應用程序中的窗口,並用這個WindowState來存儲,查詢和控制窗口的狀態。

ActivityManagerService與WindowManagerService需要緊密配合在一起工作,因爲無論是創建還是銷燬Activity都牽涉到Actiivty對象和窗口對象的創建和銷燬。這兩者是既相互獨立,又緊密關聯在一起的。

Task和Stack

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章