Android 學習筆記核心篇

基礎知識

底層原理

  • Android 操做系統是一個多用戶 Linux 操做系統,每一個應用都是一個用戶
  • 操做系統通常會給每一個應用分配一個惟一的 Linux 用戶 ID,這個 ID 對應用是不可見的。但有些狀況下兩個應用能夠共享同一個 Linux 用戶 ID,此時他們能夠訪問彼此的文件,甚至還能夠運行在同一個 Linux 進程中,共享同一個虛擬機。但兩個應用的簽名必須是同樣的
  • 每一個進程都有本身的虛擬機,通常每一個應用都運行在本身的 Linux 進程中

應用組件

  • 應用沒有惟一的入口,沒有 main() 函數,由於應用是由多個組件拼湊在一塊兒的,每一個組件都是系統或者用戶進入應用的入口,組件之間既能夠是相互獨立的,也能夠是相互依賴的。系統和其它應用在被容許的狀況下能夠啓動/激活一個應用的任意一個組件
  • 組件有四種類型: ActivityServiceBroadcastReceiverContentProvider

Activity

  • Activity 表示一個新的用戶界面,只能由系統進行建立和銷燬,應用只能監聽到一些生命週期回調,這些回調一般也被叫做生命週期方法
  • Activity 的名字一旦肯定好就不要再更改了,不然可能會引起一系列問題

Service

  • Service 表示一個後臺服務,Service 能夠是獨立的,能夠在應用退出後繼續運行。也能夠綁定到其餘進程或 Activity,表示其餘進程想使用這個 Service,像輸入法、動態壁紙、屏保等系統功能都是以 Service 的形式存在的,在須要運行的時候進行綁定
  • 大部分狀況下,更建議使用 JobScheduler,由於 JobSchedulerDoze API 配合下通常會比簡單使用 Service 更省電

BroadcastReceiver

  • BroadcastReceiver 是一個事件傳遞的組件,經過它應用能夠響應系統範圍的廣播通知。系統的包管理器會在安裝應用時將應用中的靜態廣播接收器註冊好,因此即便應用沒在運行,系統也能把事件傳遞到該組件。
  • 經過 BroadcastReceiver 能夠實現進程間通訊

ContentProvider

  • ContentProvider 是在多個應用間共享數據的組件,若是應用的一些數據想要被其它應用使用,必須經過 ContentPrivider 進行管理,不過應用的私有數據也能夠經過 ContentProvider 進行管理,主要仍是由於 ContentProvider 提供了共享數據的抽象,使用者不須要知道數據到底是以文件形式仍是數據庫等其餘形式存儲的,只須要經過 ContentProvider 提供的 統一的 API 進行數據的增刪改查便可。同時 ContentProvider 還提供了 安全 環境,能夠根據須要方便地控制數據的訪問權限,不須要手動控制文件權限或數據庫權限
  • 爲了安全,也爲了方便,通常須要經過 ContentResolver 操做 ContentProvider
  • 經過 ContentProvider 能夠實現進程間通訊

激活組件

  • 應用不能也不該該直接激活其它應用的任意一個組件,可是系統能夠,因此要想激活一個組件,須要給系統發一個消息詳細說明你的意圖( Intent ),以後系統就會爲你激活這個組件
  • ActivityServiceBroadcastReceiver 都須要經過被稱爲 Intent 的異步消息激活
  • 被激活組件返回的結果也是 Intent 形式的
  • ContentProvider 只有在收到 ContentResolver 的請求時纔會被激活
  • 只有 BroadcastReceiver 能夠不在 manifest 文件中註冊,由於有些 BroadcastReceiver 須要在程序運行時動態地註冊和註銷。而其它組件必須在 manifest 文件中註冊,不然沒法被系統記錄,也就沒法被激活
  • 若是 Intent 經過組件類名顯式指明瞭惟一的目標組件,那麼這個 Intent 就是顯式的,不然就是隱式的。隱式 Intent 通常只描述要執行動做的類型,必要時能夠攜帶數據,系統會根據這個隱式 Intent 的描述決定激活哪一個組件,若是有多個組件符合激活條件,系統通常會彈出選擇框讓用戶選擇到底激活哪一個組件
  • Service 必須使用顯式 Intent 激活,不能聲明 IntentFilter
  • 啓動指定的 Activity 使用顯式 Intent,啓動隨便一個能完成指定工做的 Activity 使用隱式 Intent。能完成指定工做的那些想要被隱式 Intent 激活的 Activity 須要事先聲明好 IntentFilter 表示本身有能力處理什麼工做,IntentFilter 通常經過 能完成的動做 、意圖類型 和 額外數據 來描述
  • 要想被隱式 Intent 激活,意圖類型至少要包含 android.intent.category.DEFAULT 的意圖類型
  • 在使用隱式 Intent 激活 Activity 以前必定要檢查一下有沒有 Activity 能處理這個 Intent :
if (sendIntent.resolveActivity(getPackageManager()) != null) {
    startActivity(sendIntent);
}
複製代碼
PackageManager packageManager = getPackageManager();
List<ResolveInfo> activities = packageManager.queryIntentActivities(intent,
        PackageManager.MATCH_DEFAULT_ONLY);
boolean isIntentSafe = activities.size() > 0;
複製代碼
  • 使用隱式 Intent 時每次都強制用戶選擇一個組件激活:
Intent intent = new Intent(Intent.ACTION_SEND);
String title = getResources().getString(R.string.chooser_title);
Intent chooser = Intent.createChooser(intent, title);
if (intent.resolveActivity(getPackageManager()) != null) {
    startActivity(chooser);
}
複製代碼
  • 若是想要你的 Activity 能被隱式 Intent 激活,若是想要某個 連接 能直接跳轉到你的 Activity,必須配置好 IntentFilter。這種連接分爲兩種: Deep linksAndroid App Links
  • Deep links 對連接的 scheme 沒有要求,對系統版本也沒有要求,也不會驗證連接的安全性,不過須要一個 android.intent.action.VIEW 的 action 以便 Google Search 能直接打開,須要 android.intent.category.DEFAULT 的 category 才能響應隱式 Intent,須要 android.intent.category.BROWSABLE 的 category 瀏覽器打開連接時才能跳轉到應用,因此經典用例以下。一個 intent filter 最好只聲明一個 data 描述,不然你得考慮和測試全部變體的狀況。系統處理這個連接的流程爲: 若是用戶以前指定了打開這個連接的默認應用就直接打開這個應用 → 若是隻有一個應用能夠處理這個連接就直接打開這個應用 → 彈窗讓用戶選擇用哪一個應用打開
<activity android:name="com.example.android.GizmosActivity" android:label="@string/title_gizmos" >
    <intent-filter android:label="@string/filter_view_http_gizmos">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <!-- Accepts URIs that begin with "http://www.example.com/gizmos」 -->
        <data android:scheme="http" android:host="www.example.com" android:pathPrefix="/gizmos" />
        <!-- note that the leading "/" is required for pathPrefix-->
    </intent-filter>
    <intent-filter android:label="@string/filter_view_example_gizmos">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <!-- Accepts URIs that begin with "example://gizmos」 -->
        <data android:scheme="example" android:host="gizmos" />
    </intent-filter>
</activity>
複製代碼
  • Android App Links 是一種特殊的 Deep links,要求連接必須是你本身網站的 HTTP URL 連接,系統版本至少是 Android 6.0 (API level 23),優勢是安全且具體,其餘應用不能使用你的連接,不過你得先 驗證你的連接,因爲連接和網站連接一致因此能夠無縫地在應用和網站間切換,能夠支持 Instant App,能夠經過瀏覽器、谷歌搜索 APP、系統屏幕搜索、甚至 Google Assistant 的連接直接跳轉到應用。驗證連接的流程爲: 將 <intent-filter> 標籤的 android:autoVerify 設置爲 true 以告訴系統自動驗證你的應用屬於這個 HTTP URL 域名 → 填寫好網站域名和應用 ID 並使用簽名文件生成 Digital Asset Links JSON 文件 → 將文件上傳到服務器,訪問路徑爲 https://domain.name/.well-known/assetlinks.json ,響應格式爲 application/json,子域名也須要存在對應的文件,一個域名能夠關聯多個應用,一個應用也能夠關聯多個域名,且可使用相同的簽名 → 利用編輯器插件完成關聯並驗證
  • 使用 Intent Scheme URL 須要作過濾。若是瀏覽器支持 Intent Scheme Uri 語法,若是過濾不當,那麼惡意用戶可能經過瀏覽器 js 代碼進行一些惡意行爲,好比盜取 cookie 等。因此若是使用了 Intent#parseUri() 方法,獲取的 intent 必須嚴格過濾,intent 至少包含 addCategory(「android.intent.category.BROWSABLE」)setComponent(null)setSelector(null) 3 個策略
  • 開放的 Activity/Service/BroadcastReceiver 等須要對傳入的 intent 作合法性校驗

應用資源

  • 添加資源限定符的順序爲: SIM 卡所屬的國家代碼和移動網代碼 → 語言區域代碼 → 佈局方向 → 最小寬度 → 可用寬度 → 可用高度 → 屏幕大不大 → 屏幕長不長 → 屏幕圓不圓 → 屏幕色域寬不寬 → 屏幕支持的動態範圍高不高 → 屏幕方向 → 設備的 UI 模式 → 夜間模式 → 屏幕像素密度 → 觸摸屏類型 → 鍵盤類型 → 主要的文字輸入方式 → 導航鍵是否可用 → 主要的非觸摸導航方式 → 支持的 API level
  • 一個資源目錄的每種資源限定符最多隻能出現一次
  • 必須提供缺省的資源文件
  • 資源目錄名是大小寫不敏感的
  • drawable 資源取別名:
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <drawable name="icon">@drawable/icon_ca</drawable>
</resources>
複製代碼
  • 佈局文件取別名:
<?xml version="1.0" encoding="utf-8"?>
<merge>
    <include layout="@layout/main_ltr"/>
</merge>
複製代碼
  • 只有動畫、菜單、raw 資源 以及 xml/ 目錄中的資源不能使用別名
  • 尋找使用最優資源的流程:
    res
  • 在應用程序運行時,設備的配置可能會發生變化(如屏幕方向變化、切換到多窗口模式,切換了系統語言),默認狀況下系統會銷燬重建正在運行的 Activity ,因此應用程序必須保證銷燬重建的過程當中用戶的數據和頁面狀態無缺無損地恢復。若是不想系統銷燬重建你的 Activity 只須要在 manifest 文件的 <activity> 標籤的 android:configChanges 屬性中添加你想本身處理的配置更改,多個配置使用 "|" 隔開,此時系統就不會在這些配置更改後銷燬重建你的這個 Activity 而是直接調用它的 onConfigurationChanged() 回調方法,你須要在這個回調中本身處理配置更改後的行爲。
  • Activity 的銷燬重建不但發生在設備配置更改後,只要用戶離開了某個 Activity,那麼那個 Activity 就隨時可能被系統銷燬。因此銷燬重建是沒法避免的,也不該該逃避,而是應該想辦法保存和恢復狀態
  • 因爲各類各樣的硬件都能安裝 Android 操做系統,Android 操做系統之間也可能千差萬別,而應用程序的一些功能是與這些軟硬件息息相關的,如拍照應用須要設備必須有攝像頭才能正常工做。應用能夠經過 <uses-feature> 標籤聲明只有知足這些軟硬件要求的設備才能安裝,經過它的 android:required 屬性設置該要求是否是必須的,程序中能夠經過 PackageManager.hasSystemFeature() 方法判斷

核心知識

Activity 相關

生命週期方法

  • Activity 變得對用戶可見時,將會回調 onStart(), 當 Activity 變得能夠和用戶交互時,將會回調 onResume()
  • onPause() 被調用時 Activity 可能依然對用戶所有可見,如多窗口模式下沒有得到焦點時,因此在 onResume() 中申請資源在 onPause() 中釋放資源的想法並不老是合理的
  • onStop() 被調用時表示 Activity 已經徹底不可見了,此時應該儘可能中止包含動畫在內的 UI 更新,儘可能釋放暫時不用的資源。對於 stopped 的 Activity,系統隨時可能殺掉包含這個 Activity 的進程,若是沒有合適的機會能夠在 onStop() 中保存一些數據
  • 若是系統在未經用戶容許的狀況下銷燬了 Activity(殺掉了該 Activity 實例所在的進程),那麼系統確定記得這個實例存在過,在用戶從新回到這個 Activity 時會從新建立一個新的實例,並將以前保存好的實例狀態傳遞給這個新的實例。這個系統以前保存好的用來恢復 Activity 狀態的數據被稱爲實例狀態(Instance state),實例狀態是以鍵值對的形式存儲在 Bundle 對象中的,默認系統只能自動存儲和恢復有 ID 的 View 的簡單狀態(如輸入框的文本,滾動控件的滾動位置),但因爲在主線程中序列化或反序列化 Bundle 對象既消耗時間又消耗系統進程內存,因此最好只用它保存簡單、輕量的數據
  • onSaveInstanceState() 被調用的時機: 對於 Build.VERSION_CODES.P 及以後的系統該方法會在 onStop() 以後隨時可能被調用,對於以前的系統該方法會在 onStop() 以前隨時被調用
  • onRestoreInstanceState() 被調用的時機: 若是有實例狀態要恢復那麼必定會在 onStart() 以後被調用
  • onActivityResult() 被調用時機: onResume() 以前。目標 Activity 沒有顯式返回任何結果或者崩潰那麼 resultCode 就會是 RESULT_CANCELED
  • 在保存實例狀態以後恢復實例狀態以前的一些操做(如 Fragment 的事務提交)是不容許的,Android 系統會不惜一切代價避免狀態丟失。Activity#onCreate() 方法中提交事務是沒問題的,由於你能夠在裏面根據保存的狀態重建,可是在其餘生命週期回調中提交事務就可能會出現問題了。FragmentActivity#onPostResume() 方法中調用了 FragmentActivity#onResumeFragments() 方法完成其關聯的全部的 Fragment 的 resume 事件的分發,執行完這兩個方法 Activity 和它關聯的全部 Fragment 纔算真正的 resumed,纔算恢復了狀態,才能夠提交事務,因此若是非要在 Activity#onCreate() 以外的回調中提交事務那麼 FragmentActivity#onPostResume()FragmentActivity#onResumeFragments() 是最好的選擇。避免在異步的回調中提交事務: 由於在這些回調執行的時候很難肯定當前 Activity 正處於什麼生命週期狀態,並且忽然地提交事務更改大量 UI 會產生糟糕的用戶體驗,因此若是遇到這樣的場景能夠考慮換一種實現思路,不要隨便使用 commitAllowingStateLoss() 方法
  • 如非必須,避免使用多層嵌套的 Fragment,不然容易出現 Bug

任務和返回棧

  • Activity 能夠在 manifest 文件中定義本身應該如何與當前任務相關聯,Activity 也能夠在啓動其它 Activity 時經過 Intent 的 flag 要求其它 Activity 應該如何與當前任務相關聯,若是二者同時出現,那麼 Intent 的 flag 要求獲勝
  • launchMode 屬性默認是 standard,每次啓動這樣的 Activity 都會新建一個新的實例放入啓動它的任務中。一個新的 Intent 總會建立一個新的實例。一個任務能夠有多個該 Activity 的實例,每一個該 Activity 的實例能夠屬於不一樣的任務
  • launchMode 屬性是 singleTopActivity : 若是當前任務頂部已是這個 Activity 的實例那麼就直接將 Intent 傳遞給這個實例的 onNewIntent() 方法。一個任務能夠有多個該 Activity 的實例,每一個該 Activity 的實例能夠屬於不一樣的任務
  • launchMode 屬性是 singleTaskActivity : 若是這個 Activity 的實例已經在某個任務中存在了那麼就直接將 Intent 傳遞給這個實例的 onNewIntent() 方法,並將其所在的任務移到前臺即當前任務頂部,不然會新建一個任務並實例化一個這個 Activity 的實例放在棧底
  • launchMode 屬性是 singleInstanceActivity : 和 singleTask 相似,不過它會保證新的任務中有且僅有一個這個 Activity 的實例
  • FLAG_ACTIVITY_NEW_TASK : 行爲和 singleTask 同樣,不過在新建任務以前會先尋找是否已經存在和這個 Activity 有相同 affinity 的任務,若是已經存在就不新建任務了,而是直接在那個任務中啓動
  • FLAG_ACTIVITY_SINGLE_TOP : 行爲和 singleTop 同樣
  • FLAG_ACTIVITY_CLEAR_TOP : 若是當前任務中已經有要啓動的 Activity 的實例了,那麼就銷燬它上面全部的 Activity(甚至包括它本身),因爲 launchMode 屬性是 standardActivity 一個新的 Intent 總會建立一個新的實例,因此若是要啓動的 ActivitylaunchMode 屬性是 standard 的而且沒有 FLAG_ACTIVITY_SINGLE_TOP 的 flag,那麼這個 flag 會銷燬它本身而後建立一個新的實例
  • FLAG_ACTIVITY_CLEAR_TOPFLAG_ACTIVITY_NEW_TASK 結合使用能夠直接定位指定的 Activity 到前臺
  • 無論要啓動的 Activity 是在當前任務中啓動仍是在新任務中啓動,點擊返回鍵均可以直接或間接回到以前的 Activity,間接的狀況像 singleTask 是將整個任務而不是隻有一個 Activity 移到前臺,任務中的全部的 Activity 在點擊返回鍵的時候都要依次彈出
  • 若是離開了任務,系統可能會清除任務中除了最底層 Activity 外的的全部 Activity。將最底層 Activity<activity> 標籤的 alwaysRetainTaskState 屬性設置爲 true 能夠保留任務中全部的 Activity。將最底層 Activity<activity> 標籤的 clearTaskOnLaunch 屬性設置爲 true 能夠在不管什麼時候進入或離開這個任務都清除任務中除了最底層 Activity 外的的全部 Activity。包含最底層 Activity 在內的任何 Activity 只要 finishOnTaskLaunch 屬性設置爲 true 那麼離開任務再回來都不會出現了
  • Activity 做爲新文檔添加到最近任務中須要設置 newDocumentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);launchMode 必須是 standard 的,若是此時又設置了 newDocumentIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) 那麼系統每次都會建立新的任務並將目標 Activity 做爲根 Activity,若是沒有設置 FLAG_ACTIVITY_MULTIPLE_TASK,那麼 Activity 實例會被重用到新的任務中(若是已經存在這樣的任務就不會重建,而是直接將任務移到前臺並調用 onNewIntent()
  • <activity> 標籤的 android:documentLaunchMode 屬性默認是 none : 不會爲新文檔建立新的任務。intoExisting 與設置了 FLAG_ACTIVITY_NEW_DOCUMENT 但沒設置 FLAG_ACTIVITY_MULTIPLE_TASK 同樣。always 與設置了 FLAG_ACTIVITY_NEW_DOCUMENT 同時設置了 FLAG_ACTIVITY_MULTIPLE_TASK 同樣。nevernone 同樣不過會覆蓋 FLAG_ACTIVITY_NEW_DOCUMENTFLAG_ACTIVITY_MULTIPLE_TASK
  • 使用 Intent.FLAG_ACTIVITY_NEW_DOCUMENT|android.content.Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS; 同時 <activity> 標籤的 android:autoRemoveFromRecents 屬性設置爲 false 可讓文檔 Activity 即便結束了也能夠保留在最近任務中
  • 使用 finishAndRemoveTask() 方法能夠移除當前任務

動態申請權限

  • Android 6.0 (API level 23) 開始 targetSdkVersion >= 23 的應用必須在運行時動態申請權限
  • 權限請求對話框是操做系統進行管理的,應用沒法也不該該干預。
  • 系統對話框描述的是權限組而不是某個具體權限
  • 若是用戶授予了權限組中的一個權限,那麼再申請該權限組的其它權限時系統會自動授予,不須要用戶再受權。但這並不意味着該權限組中的其它權限就不用申請了,由於權限處於哪一個權限組未來有可能會發生變化
  • 調用 requestPermissions() 並不意味着系統必定會彈出權限請求對話框,也就是說不能假設調用該方法後就發生了用戶交互,由於若是用戶以前勾選了 「禁止後再也不詢問」 或者系統策略禁止應用獲取權限,那麼系統會直接拒絕這次權限請求,沒有任何交互
  • 若是某個權限跟應用的主要功能無關,如應用中廣告可能須要位置權限,用戶可能很費解,此時在申請權限以前彈出對話框向用戶解釋爲何須要這個權限是個不錯的選擇。但不要在全部申請權限以前都彈出對話框解釋,由於頻繁地打斷用戶的操做或讓用戶進行選擇容易讓用戶不耐煩
  • Fragment 中的 onRequestPermissionsResult() 方法只有在使用 Fragment#requestPermissions() 方法申請權限時纔可能接收到回調,建議將權限放在所屬 Activity 中申請和處理
  • 應用應該儘可能少地申請權限,像讓用戶拍一張照片或者選擇一張圖片徹底不須要相機權限和外存權限,能夠經過隱式 Intent 拉起系統相機或其餘應用完成,應用只須要在 onActivityResult() 回調中接收數據就好了。可是有一點必定要注意,若是你在 AndroidManifest.xml 文件中聲明瞭相機權限,你就必須得動態申請並得到相機權限才能拉起系統相機
// 請求通信錄權限的模板代碼以下
private void showContactsWithPermissionsCheck() {
    if (ContextCompat.checkSelfPermission(MainActivity.this,
            Manifest.permission.READ_CONTACTS)
            != PackageManager.PERMISSION_GRANTED) {
        if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
                Manifest.permission.READ_CONTACTS)) {
            // TODO: 彈框解釋爲何須要這個權限. 【下一步】 -> 再次請求權限
        } else {
            ActivityCompat.requestPermissions(MainActivity.this,
                    new String[]{Manifest.permission.READ_CONTACTS},
                    RC_CONTACTS);
        }
    } else {
        showContacts();
    }
}
private void showContacts() {
    startActivity(ContactsActivity.getIntent(MainActivity.this));
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    switch (requestCode) {
        case RC_CONTACTS:
            if (grantResults.length > 0
                    && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                showContacts();
            } else {
                if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
                        Manifest.permission.READ_CONTACTS)) {
                    // TODO: 彈框引導用戶去設置頁主動授予該權限. 【去設置】 -> 應用信息頁
                } else {
                    // TODO: 彈框解釋爲何須要這個權限. 【下一步】 -> 再次請求權限
                }
            }
            break;
        default:
            break;
    }
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == RC_SETTINGS) {
        // TODO: 在用戶主動授予權限後從新檢查權限,但不要在這裏進行事務提交等生命週期敏感操做
    }
}
複製代碼

Shortcut

  • 相似於 iOS 的 3D Touch,長按啓動圖標彈出幾個快捷入口,入口最好不要超過 4 個,像搜索、掃描二維碼、發帖等應用程序最經常使用功能的入口被稱爲靜態 shortcut,不會隨着用戶不一樣或隨着用戶使用而改變。還有一種像從某個存檔點繼續遊戲、任務進度等與用戶相關的上下文敏感入口被稱爲動態 shortcut,會因用戶不一樣或隨着用戶使用不斷變化。還有一種在 Android 8.0 (API level 26) 及以上系統版本上像固定網頁標籤等用戶主動固定到桌面的快捷方式被稱爲固定 shortcut
  • 靜態 shortcut 系統能夠自動備份和恢復,動態 shortcut 須要應用本身備份和恢復,固定 shortcut 的圖標系統沒法備份和恢復所以須要應用本身完成
  • android:shortcutIdandroid:shortcutShortLabel 屬性是必須的,android:shortcutShortLabel 不能超過 10 個字符,android:shortcutLongLabel 不能超過 25 個字符,android:icon 不能包含 tint
  • 獲取 ShortcutManager 的方式有兩個: getSystemService(ShortcutManager.class)getSystemService(Context.SHORTCUT_SERVICE)
  • 建立固定 shortcut:
ShortcutManager mShortcutManager =
        context.getSystemService(ShortcutManager.class);
if (mShortcutManager.isRequestPinShortcutSupported()) {
    ShortcutInfo pinShortcutInfo =
            new ShortcutInfo.Builder(context, "my-shortcut").build();
    Intent pinnedShortcutCallbackIntent =
            mShortcutManager.createShortcutResultIntent(pinShortcutInfo);
    PendingIntent successCallback = PendingIntent.getBroadcast(context, 0,
            pinnedShortcutCallbackIntent, 0);
    mShortcutManager.requestPinShortcut(pinShortcutInfo,
            successCallback.getIntentSender());
}
複製代碼

其它

  • Parcelable 對象用來在進程間、Activity 間傳遞數據,保存實例狀態也是用它,Bundle 是它的一個實現,最好只用它存儲和傳遞少許數據,別超過 50k,不然既可能影響性能又可能致使崩潰
  • Android 9 (API level 28) 開始廢棄了 Loader API,包括 LoaderManagerCursorLoader 等類的使用。推薦使用 ViewModelLiveDataActivityFragment 生命週期中加載數據
  • Activity 能夠經過 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 保持屏幕常亮,這是最推薦、最簡單、最安全的保持屏幕常亮的方法,給 view 添加 android:keepScreenOn="true" 也是同樣的。這個只在這個 Activity 生命週期內有效,因此大可放心,若是想提早解除常亮,只須要清除這個 flag 便可
  • WAKE_LOCK 能夠阻止系統睡眠,保持 CPU 一直運行,須要 android.permission.WAKE_LOCK 權限,經過 powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::MyWakelockTag") 建立實例,經過 wakeLock.acquire() 方法請求鎖,經過 wakelock.release() 釋放鎖
  • WakefulBroadcastReceiver 結合 IntentService 也能夠阻止系統睡眠

UI 相關

系統欄適配

  • Android 4.1 (API level 16) 開始能夠經過 setSystemUiVisibility() 方法在各個 view 層次中(通常是在 DecorView 中)配置 UI flag 實現系統欄(狀態欄、導航欄統稱)配置,最終彙整體現到 window 級
  • View.SYSTEM_UI_FLAG_FULLSCREEN 能夠隱藏狀態欄,View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 能夠隱藏導航欄。可是: 用戶的任何交互包括觸摸屏幕都會致使 flag 被清除進而系統欄保持可見,一旦離開當前 Activity flag 就會被清除,因此若是在 onCreate() 方法中設置了這個 flag 那麼按 HOME 鍵再回來狀態欄又保持可見了,非要這樣設置的話通常要放在 onResume()onWindowFocusChanged() 方法中,並且這樣設置只有在目標 View 可見時纔會生效,狀態欄/導航欄的顯示隱藏會致使顯示內容的大小尺寸跟着變化。
  • View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 可讓內容顯示在狀態欄後面,View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 可讓內容顯示在導航欄後面,這樣不管系統欄顯示仍是隱藏內容都不會跟着變化,但不要讓可交互的內容出如今系統欄區域內,經過將 android:fitsSystemWindows 屬性設置爲 true 可讓父容器調整 padding 以便爲系統欄留出空間,若是想自定義這個 padding 能夠經過覆寫 View 的 fitSystemWindows(Rect insets) 方法(API level 20 以上覆寫 onApplyWindowInsets(WindowInsets insets) 方法)完成
  • lean back 全屏模式: View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION,隱藏狀態欄和導航欄,任何交互都會清除 flag 使系統欄保持可見
  • Immersive 全屏模式: View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION,隱藏狀態欄和導航欄,從被隱藏的系統欄邊緣向內滑動會使系統欄保持可見,應用沒法響應這個手勢
  • sticky immersive 全屏模式: View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION,隱藏狀態欄和導航欄,從被隱藏的系統欄邊緣向內滑動會使系統欄暫時可見,flag 不會被清除,且系統欄的背景是半透明的,會覆蓋應用的內容,應用也能夠響應這個手勢,在用戶沒有任何交互或者沒有系統欄交互幾秒鐘後系統欄會自動隱藏
  • 真正的沉浸式全屏體驗須要 6 個 flag: View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN
  • 監聽系統欄可見性(sticky immersive 全屏模式沒法監聽):
decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {
    @Override
    public void onSystemUiVisibilityChange(int visibility) {
        if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
            // TODO: The system bars are visible. Make any desired
        } else {
            // TODO: The system bars are NOT visible. Make any desired
        }
    }
});
複製代碼
  • 全面屏適配只須要指定支持的最大寬高比便可: <meta-data android:name="android.max_aspect" android:value="2.4"/>
  • Android 9 (API level 28) 開始支持劉海屏 cutout 的配置,window 的屬性 layoutInDisplayCutoutMode 默認是 LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT,豎屏時能夠渲染到劉海區,橫屏時不容許渲染到劉海區。LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 橫豎屏均可以渲染到劉海區。LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER 橫豎屏都不容許渲染到劉海區,能夠在 values-v28/styles.xml 文件中經過 android:windowLayoutInDisplayCutoutMode 指定默認的劉海區渲染模式
  • 華爲手機經過 <meta-data android:name="android.notch_support" android:value="true" /> 屬性聲明應用是否已經適配了劉海屏,若是沒適配,那麼在橫屏或者豎屏不顯示狀態欄時會禁止渲染到劉海區,開發者文檔: 《華爲劉海屏手機安卓O版本適配指導》
  • 小米手機經過 <meta-data android:name="notch.config" android:value="portrait|landscape" /> 設置默認的劉海區渲染模式,開發者文檔: 《小米劉海屏 Android O 適配》《小米劉海屏 Android P 適配》
  • 其餘手機的開發者文檔有: OPPO 手機的 《OPPO凹形屏適配說明》,VIVO 手機的 《異形屏應用適配指南》,錘子手機的 《Smartisan 開發者文檔》
  • Android 5.0 (API level 21) 開始支持經過 window 的 setStatusBarColor() 方法設置狀態欄背景色,要求 window 必須添加 WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 的 flag 而且清除 WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS 的 flag
  • Android 6.0 (API level 23) 開始能夠經過 setSystemUiVisibility() 方法設置 View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR flag 兼容亮色背景的狀態欄,一樣要求 window 必須添加 WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 的 flag 而且清除 WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS 的 flag
  • 小米手機在 MIUI 開發版 7.7.13 以前須要經過反射兼容亮色背景的狀態欄,開發者文檔: 《MIUI 9 & 10「狀態欄黑色字符」實現方法變動通知》
  • 魅族手機一樣須要經過反射兼容亮色背景的狀態欄,開發者文檔: 《狀態欄變色》

動畫

  • view 動畫系統只能做用於 view 對象,只能改變 view 的部分樣式,只是簡單改變了 view 繪製,並無改變 view 真正的位置和屬性。核心類是 android.view.animation.Animation 和它的 ScaleAnimation 等子類,通常使用 AnimationUtils.loadAnimation() 方法加載。不建議使用,除非爲了方便又能知足如今和未來的需求
  • 屬性動畫系統是一個健壯的、優雅的動畫系統,能夠對任意對象的屬性作動畫。核心類是 android.animation.Animator 的子類 ValueAnimatorObjectAnimatorAnimatorSet
  • 經過調用 ValueAnimatorofInt()ofFloat() 等工廠方法獲取 ValueAnimator 對象,經過它的 addUpdateListener() 方法能夠監聽動畫值並在裏面進行自定義操做
  • ObjectAnimator 做爲 ValueAnimator 的子類能夠自動地爲目標對象的命名屬性設置動畫,可是對目標對象有嚴格的要求: 目標對象必須有對應屬性的 setter 方法,若是在工廠方法中只提供了一個動畫值那麼它會做爲終止值,起始值爲目標對象的當前值,此時爲了獲取當前屬性值目標對象必須有對應屬性的 getter 方法。有些屬性的更改不會致使 view 從新渲染,此時須要主動調用 invalidate() 方法強制觸發重繪
  • AnimatorListenerAdapter 提供了 Animator.AnimatorListener 接口的空實現
  • 多數狀況下能夠直接使用系統提供的幾個動畫 duration,如 getResources().getInteger(android.R.integer.config_shortAnimTime)
  • 能夠調用任意 view 對象的 animate() 方法獲取 ViewPropertyAnimator 對象,鏈式調用這個對象的 scaleX()alpha() 等方法能夠簡單方便地同時對 view 的多個屬性作動畫
  • 爲了更好地重用和管理屬性動畫,最好使用 XML 文件來描述動畫並放到 res/animator/ 目錄下,ValueAnimator 對應 <animator>ObjectAnimator 對應 <objectAnimator>AnimatorSet 對應 <set>,使用 AnimatorInflater.loadAnimator() 能夠加載這些動畫
  • 動態 Drawable 的實現有兩種,最傳統最簡單的就是像電影關鍵幀同樣依次指定關鍵幀和每一幀的停留時間,AnimationDrawable 對應於 XML 文件中的 <animation-list>,保存目錄爲 res/drawable/AnimationDrawablestart() 方法能夠在 onStart() 中調用。還有一種是 AnimatedVectorDrawable,須要 res/drawable/ 中的 <animated-vector> 引用 res/drawable/ 中的 <vector> 對其使用 res/animator/ 中的 <objectAnimator> 動畫
  • 忽然更改顯示的內容會讓視覺感覺很是突兀不和諧,並且可能意識不到哪些內容忽然變了,因此不少場景下須要使用動畫過渡一下,而不是忽然更改顯示的內容
  • 顯示隱藏 view 的經常使用動畫有三個: crossfade 動畫,card flip 動畫,circular reveal 動畫
  • crossfade 動畫就是內容淡出另外一個內容淡入交叉進行,也被稱爲溶入動畫。實現方式爲: 事先將淡入 view 的 visibility 設置爲 GONE → 開始動畫時將淡入 view 的 alpha 設置爲 0,visibility 設置爲 VISIBLE → 將淡入 view 的 alpha 動畫到 1,將淡出 view 的 alpha 動畫到 0 並在動畫結束時將淡出 view 的 visibility 設置爲 GONE
  • card flip 動畫就是卡片翻轉動畫,須要四個動畫描述: card_flip_right_incard_flip_right_outcard_flip_left_incard_flip_left_out
  • Android 5.0 (API level 21) 開始支持 circular reveal 圓形裁剪動畫,實現方式爲: 事先將 view 的 visibility 設置爲 INVISIBLE → 利用 ViewAnimationUtils.createCircularReveal() 方法建立半徑從 0 到 Math.hypot(cx, cy) 的圓形裁剪動畫 → 將 view 的 visibility 設置爲 VISIBLE 而後開啓動畫
  • 直線動畫移動 view 只須要藉助 ObjectAnimator.ofFloat() 方法動畫設置 view 的 translationXtranslationY 屬性便可
  • 曲線動畫移動 view 還須要藉助 Android 5.0 (API level 21) 開始提供的 PathInterpolator 插值器(對應於 XML 文件中的 <pathInterpolator>),他須要個 Path 對象描述運動的貝塞爾曲線。可使用 ObjectAnimator.ofFloat(view, "translationX", 100f) 同時設置 PathInterpolator 也能夠直接設置 view 動畫路徑 ObjectAnimator.ofFloat(view, View.X, View.Y, path)。系統提供的 fast_out_linear_in.xmlfast_out_slow_in.xmllinear_out_slow_in.xml 三個基礎的曲線插值器能夠直接使用
  • 基於物理的動畫須要引用 support-dynamic-animation 支持庫,最多見的就是 FlingAnimationSpringAnimation 動畫,物理動畫主要是模擬現實生活中的物理世界,利用經典物理學的知識和原理實現動畫過程,其中最關鍵的就是的概念。FlingAnimation 就是用戶經過手勢給動畫元素一個力,動畫元素在這個力的做用下運動,以後因爲摩擦力的存在慢慢減速直到結束,固然這個力也能夠經過程序直接指定(指定固定的初始速度)。SpringAnimation 就是彈簧動畫,動畫元素的運動與彈簧有關
  • FlingAnimation 經過 setStartVelocity() 方法設置初始速度,經過 setMinValue()setMaxValue() 約束動畫值的範圍,經過 setFriction() 設置摩擦力(若是不設置默認爲 1)。若是動畫的屬性不是以像素爲單位的,那麼須要經過 setMinimumVisibleChange() 方法設置用戶可察覺到動畫值的最小更改,如對於 TRANSLATION_XTRANSLATION_YTRANSLATION_ZSCROLL_XSCROLL_Y 1 像素的更改就對用戶可見了,而對於 ROTATIONROTATION_XROTATION_Y 最小可見更改是 MIN_VISIBLE_CHANGE_ROTATION_DEGREES 即 1/10 像素,對於 ALPHA 最小可見更改是 MIN_VISIBLE_CHANGE_ALPHA 即 1/256 像素,對於 SCALE_XSCALE_Y 最小可見更改是 MIN_VISIBLE_CHANGE_SCALE 即 1/500 像素,計算公式爲: 自定義屬性值的範圍 / 動畫的變化像素範圍。
  • SpringAnimation 須要先鞏固一下彈簧的知識,彈簧有一個屬性叫阻尼比 ζ(damping ratio),是實際的粘性阻尼係數 C 與臨界阻尼係數 Cr 的比。ζ = 1 時爲臨界阻尼,這是最小的能阻止系統震盪的狀況,系統能夠最快回到平衡位置。0 < ζ < 1 時爲欠阻尼,物體會做對數衰減振動。ζ > 1 時爲過阻尼,物體會沒有振動地緩慢回到平衡位置。ζ = 0 表示不考慮阻尼,震動會一直持續下去不會中止。默認是 SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY 即 0.5,能夠經過 getSpring().setDampingRatio() 設置。彈簧另外一個屬性叫剛度(stiffness),剛度越大形變產生的力就越大,默認是 SpringForce.STIFFNESS_MEDIUM 即 1500.0,能夠經過 getSpring().setStiffness() 設置
    damping ratio
  • FlingAnimationSpringAnimation 動畫經過 setStartVelocity() 設置固定的初始速度時最好用 dp/s 轉成 px/s : TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpPerSecond, getResources().getDisplayMetrics()),用戶手勢的初始速度能夠經過 GestureDetector.OnGestureListenerVelocityTracker 計算
  • SpringAnimation 動畫使用 start() 方法開始動畫時屬性值不會立刻變化,而是在每次動畫脈衝即繪製以前更改。animateToFinalPosition() 方法會立刻設置最終的屬性值,若是動畫沒開始就開始動畫,這在鏈式依賴的彈簧動畫中很是有用。cancel() 方法能夠結束動畫在其當前位置,skipToEnd() 方法會跳轉至終止值再結束動畫,能夠經過 canSkipToEnd() 方法判斷是不是阻尼動畫
  • 放大預覽動畫只須要同時動畫更改目標 view 的 XYSCALE_XSCALE_Y 屬性便可,不過要先計算好兩個 view 最終的位置和初始縮放比
  • Android 提供了預加載的佈局改變更畫,能夠經過 android:animateLayoutChanges="true" 屬性告訴系統開啓默認動畫,或者經過 LayoutTransition API 設置
  • Activity 內部的佈局過渡動畫: 過渡動畫框架能夠在開始 Scene 和結束 Scene 開始過渡動畫,Scene 存儲着 view hierarchy 狀態,包括全部 view 和其屬性值,開始 Scene 能夠經過 setExitAction() 定義過渡動畫開始前要執行的操做,結束 Scene 能夠經過 Scene.setEnterAction() 定義過渡動畫完成後要執行的操做。若是 view hierarchy 是靜態不變的,能夠經過佈局文件描述和加載 Scene.getSceneForLayout(mSceneRoot, R.layout.a_scene, this),不然能夠手動建立 new Scene(mSceneRoot, mViewHierarchy)Transition 的內置子類包括 AutoTransitionFadeChangeBounds,能夠在 res/transition/ 目錄下定義內置的 <fade xmlns:android="http://schemas.android.com/apk/res/android" />,多個組合包裹在 <transitionSet> 標籤中,而後使用 TransitionInflater.from(this).inflateTransition(R.transition.fade_transition) 加載。還能夠手動建立 new Fade()。開始過渡動畫時只須要執行 TransitionManager.go(mEndingScene, mFadeTransition) 便可。默認是對 Scene 中全部的 view 做動畫,能夠經過 addTarget()removeTarget() 在開始過渡動畫前進行調整。若是不想在兩個 view hierarchy 間進行過渡,而是在同一個 view hierarchy 狀態更改後執行過渡動畫,那就不須要使用 Scene 了,先利用 TransitionManager.beginDelayedTransition(mRootView, mFade) 讓系統記錄 view 的更改,而後增刪 view 來更改 view hierarchy 的狀態,系統會在重繪 UI 時執行延遲過渡動畫。因爲 SurfaceView 由非 UI 線程更新,因此它的過渡可能有問題,TextureView 在一些過渡類型上可能有問題,AdapterView 與過渡動畫框架不兼容,TextView 的大小過渡動畫可能有問題
  • Activity 之間的過渡動畫: 須要 Android 5.0 (API level 21) ,內置的進入退出過渡動畫包括: explode 從中央進入或退出,slide 從一邊進入或退出,fade 透明度漸變進入或退出。內置的共享元素過渡動畫包括: changeBounds 動態更改目標 view 的邊界,changeClipBounds 動態裁剪目標 view 的邊界,changeTransform 動態更改目標 view 的縮放和旋轉,changeImageTransform 動態更改目標 view 的縮放和尺寸。過渡動畫須要兩個 Activity 都要開啓 window 的內容過渡: android:windowActivityTransitions 屬性設置爲 true 或者代碼中手動 getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS) 開啓。setExitTransition()setSharedElementExitTransition() 方法能夠爲起始 Activity 設置退出過渡動畫,setEnterTransition()setSharedElementEnterTransition() 方法能夠爲目標 Activity 設置進入過渡動畫。激活目標 Activity 的時候須要攜帶 ActivityOptions.makeSceneTransitionAnimation(this).toBundle() 的 Bundle,返回的時候要使用 finishAfterTransition() 方法。共享元素須要使用 android:transitionName 屬性或者 View.setTransitionName() 方法指定名字,多個共享元素使用 Pair.create(view1, "agreedName1") 傳遞信息
  • 自定義過渡動畫須要繼承 Transition,實現 captureStartValues()captureEndValues() 方法捕獲過渡的 view 屬性值並告訴過渡框架,具體實現爲經過 transitionValues.view 檢索當前 view,經過 transitionValues.values.put(PROPNAME_BACKGROUND, view.getBackground()) 存儲屬性值,爲了不衝突 key 的格式必須爲 package_name:transition_name:property_name。同時還要實現 createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) 方法,框架調用這個方法的次數取決於開始和結束 scene 須要更改的元素數
  • 動畫可能會影響性能,必要時能夠啓用 Profile GPU Rendering 進行調試

其它

  • Android 8.0 (API level 26) 開始支持自適應啓動圖標,自適應啓動圖標必須由前景和背景兩部分組成,尺寸必須都是 108 x 108 dp,其中內部的 72 x 72 dp 用來顯示圖標,靠近四個邊緣的 18 dp 是保留區域,用來進行視覺交互
  • 對於字體大小自適應的 TextView 寬和高都不能是 wrap_contentautoSizeTextType 默認是 none,設置爲 uniform 開啓自適應,默認最小 12sp,最大 112sp,粒度 1pxautoSizePresetSizes 屬性能夠設置預置的一些大小
  • Android 8.0 (API level 26) 開始支持 XML 自定義字體,兼容庫能夠兼容到 Android 4.1 (API level 16),字體文件路徑爲 res/font/,使用屬性爲 fontFamily,獲取 TypefacegetResources().getFont(R.font.myfont);,兼容庫使用 ResourcesCompat.getFont(context, R.font.myfont)
  • Android 9 (API level 28) 支持控件放大鏡功能,Magnifiershow() 方法的參數是相對於被放大 View 的左上角的座標
  • 工程中的 Drawable 資源只能有一個狀態,你不該該手動更改它的任何屬性,不然會影響到其它使用這個 Drawable 資源的地方
  • Android 7.0 (API level 24) 開始支持在 XML 文件中使用自定義 Drawable,公共頂級類使用全限定名做爲標籤名便可 <com.myapp.MyDrawable>,公共靜態內部類可使用 class 屬性 class="com.myapp.MyTopLevelClass$MyDrawable"
  • Android 5.0 (API level 21) 開始支持爲 Drawable 設置 tint
  • Android 5.0 (API level 21) 開始支持矢量圖,支持庫能夠支持到 Android 2.1 (API level 7+),兼容低版本是須要 Gradle 插件版本大於 2.0+ 時添加 vectorDrawables.useSupportLibrary = true 並使用 VectorDrawableCompatAnimatedVectorDrawableCompat

BroadcastReceiver 相關

  • Android 9 (API level 28) 開始 NETWORK_STATE_CHANGED_ACTION 廣播再也不包含 SSID,BSSID 等信息
  • Android 8.0 (API level 26) 開始限制應用靜態註冊一些非當前應用專屬的隱式廣播的 BroadcastReceiver,免除這項限制的廣播包括 ACTION_LOCKED_BOOT_COMPLETED 不太可能影響用戶體驗的廣播
  • Android 7.0 (API level 24) 開始不能發送和接收 ACTION_NEW_PICTUREACTION_NEW_VIDEO 系統廣播,能夠經過 JobInfoJobParameters 完成。不能靜態註冊 CONNECTIVITY_ACTION 廣播,若是想在網絡變化時調度任務能夠選擇使用 WorkManager,若是隻在應用運行期間監聽網絡變化使用 ConnectivityManager 比動態註冊註銷 BroadcastReceiver 更優雅
  • 應該儘可能在代碼中動態註冊註銷 BroadcastReceiver
  • onReceive() 方法中不能進行復雜工做不然會致使 ANR,onReceive() 方法一旦執行完,系統可能就認爲這個廣播接收器已經沒用了,隨時會殺掉包含這個廣播接收器的進程,包括這個進程啓動的線程。使用 goAsync() 方法能夠在 PendingResult#finish() 方法執行前爲廣播接收器的存活爭取更多的時間,但最好仍是使用 JobScheduler 等方式進行長時間處理工做
  • 使用 sendBroadcast() 方法發的廣播屬於常規廣播,全部能接收這個廣播的廣播接收器接收到廣播的順序是不可控的
  • 使用 sendOrderedBroadcast() 方法發的廣播屬於有序廣播,根據廣播接收器的優先級一個接一個地傳遞這條廣播,相同優先級的順序不可控,廣播接收器能夠選擇繼續傳遞給下一個,也能夠選擇直接丟掉
  • 使用 LocalBroadcastManager.getInstance(this).sendBroadcast() 方法發的廣播屬於應用進程內的本地廣播,這樣的廣播只有應用本身知道,比系統級的全局廣播更安全更有效率
  • 爲了保證廣播的 action 全局惟一,action 的名字最好使用應用的包名做爲前綴,最好聲明成靜態字符串常量

數據存儲與共享

存儲方式

  • 系統會在安裝應用時在內部存儲器的文件系統中爲應用生成一個私有文件目錄,通常是 /data/data/your.application.package//data/user/0/your.application.package/,當卸載應用時這個目錄也會被刪除。這個目錄除了系統和應用本身誰都沒法訪問,除非擁有權限。這個路徑下有個 files/ 子目錄用來存儲應用的文件,能夠經過 getFilesDir() 方法獲取這個路徑表示,能夠經過 openFileOutput(filename, Context.MODE_PRIVATE) 寫這個目錄下的文件。還有一個 cache/ 子目錄用來存儲臨時緩存文件,系統可能會在存儲空間不足時清理這個目錄,能夠經過 getCacheDir() 方法獲取這個路徑表示,能夠經過 File#createTempFile(fileName, null, context.getCacheDir()) 方法在這個目錄下建立一個臨時文件。還有一個 shared_prefs/ 子目錄用來以 XML 文件的形式存儲簡單的鍵值對數據,須要使用 SharedPreferences API 進行管理
  • 讀寫外存(外存是指能夠被移除的外部存儲器)文件須要先動態申請 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 權限,而後檢查外存是否可用: Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) 表示可寫,Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state) 表示可讀。外存根目錄可使用 Environment.getExternalStorageDirectory() 方法獲取,通常是 /storage/emulated/0/,使用 new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), albumName) 能夠讀寫外存公有目錄的文件。使用 getExternalFilesDir(null) 能夠獲取該應用的外存根目錄,通常是 /storage/emulated/0/Android/data/your.application.package/files,使用 new File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), albumName) 能夠讀寫文件,應用的外存目錄也會在卸載應用時被刪除。使用 getExternalCacheDir() 能夠獲取應用的外存緩存目錄。
  • 使用 myFile.delete()myContext.deleteFile(fileName) 刪除文件
  • 直接使用 SQLite API 進行數據庫操做既麻煩又容易出錯,建議使用 Room 等其它 ORM 庫進行數據庫操做
  • 獲取 SharedPreferences 的方式有三個: 經過 PreferenceManager.getDefaultSharedPreferences() 能夠獲取或建立名字爲 context.getPackageName() + "_preferences" 模式爲 Context.MODE_PRIVATE 的文件。經過 MainActivity.this.getPreferences(Context.MODE_PRIVATE) 能夠獲取或建立名字爲當前 Activity 類名的文件。使用 context.getSharedPreferences("file1", Context.MODE_PRIVATE) 能夠獲取或建立名字是 file1 的文件。MODE_WORLD_READABLEMODE_WORLD_WRITEABLE 從 Android 7.0 (API level 24) 開始被禁止使用了。commit() 方法會將數據同步寫到磁盤因此可能會阻塞 UI,而 apply() 方法會異步寫到磁盤。

分享文件

  • 爲了安全地共享文件,分享的文件必須經過 content URI 表示,必須授予這個 content URI 臨時訪問權限。FileProvider 做爲 ContentProvider 的特殊子類,它的 getUriForFile() 靜態方法能夠爲文件生成 content URI。
<provider android:name="android.support.v4.content.FileProvider" android:authorities="com.example.myapp.fileprovider" android:grantUriPermissions="true" android:exported="false">
    <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths" />
</provider>
複製代碼
<paths>
    <files-path path="images/" name="myimages" />
</paths>
複製代碼
  • android:authorities 屬性通常是以當前應用包名爲前綴的字符串,用來標誌數據的全部者,多個的話用分號隔開
  • <files-path/> 表明 getFilesDir()
  • <cache-path/> 表明 getCacheDir()
  • <external-path/> 表明 Environment.getExternalStorageDirectory()
  • <external-files-path> 表明 getExternalFilesDir(null)
  • <external-cache-path> 表明 getExternalCacheDir()
  • <external-media-path> 表明 getExternalMediaDirs()
File imagePath = new File(getFilesDir(), "images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = FileProvider.getUriForFile(getContext(), "com.example.myapp.fileprovider", newFile);
複製代碼
  • 給 Intent 添加 FLAG_GRANT_READ_URI_PERMISSIONFLAG_GRANT_WRITE_URI_PERMISSION 的 flag 授予對這個 content URI 的臨時訪問權限,該權限會被目標 Activity 所在應用的其它組件繼承,會在所在的任務結束時自動撤銷受權
  • 調用 Context.grantUriPermission(package, Uri, mode_flags) 方法也能夠授予 FLAG_GRANT_READ_URI_PERMISSIONFLAG_GRANT_WRITE_URI_PERMISSION 權限,但只有在主動調用 revokeUriPermission() 方法後或者重啓系統後纔會撤銷受權
List<ResolveInfo> activities = getPackageManager().queryIntentActivities(intent,
        PackageManager.MATCH_DEFAULT_ONLY);
if (activities.size() > 0) {
    for (ResolveInfo resolveInfo : activities) {
        grantUriPermission(resolveInfo.activityInfo.packageName,
                outputUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    }
}
...
revokeUriPermission(outputUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
複製代碼

ContentProvider

  • ContentProvider 的數據形式和關係型數據庫的表格數據相似,所以 API 也像數據庫同樣包含增刪改查(CRUD)操做,但爲了更好地組織管理一個或多個 ContentProvider,最好經過 ContentResolver 操做 ContentProvider
  • 對於 ContentProvider 的增刪改查操做,不能直接在 UI 線程上執行
  • UriContentUris 類的靜態方法能夠方便地構造 content URI
SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;
複製代碼
mCursor = getContentResolver().query(
        UserDictionary.Words.CONTENT_URI,
        mProjection,
        mSelectionClause,
        mSelectionArgs,
        mSortOrder);
複製代碼
  • 爲了防止 SQL 注入,禁止拼接 SQL 語句,如 mSelectionClause 不能直接包含 selectionArgs 參數值
  • ContentProvider 所在應用自己的組件能夠隨便訪問它,不須要受權
  • 若是 ContentProvider 的應用不指定任何權限,那麼其它應用就沒法訪問這個 ContentProvider 的數據
  • 使用者須要事先經過 <uses-permission> 標籤獲取訪問權限
  • 建立 ContentProvider 須要繼承 ContentProvider 並實現增刪改查等一系列方法: onCreate() 在系統建立 provider 後立刻調用,能夠在這裏建立數據庫,但不要在這裏作耗時操做。getType() 返回 content URI 的 MIME 類型。query()insert()update()delete() 進行增刪改查。除了 onCreate() 方法其它方法必需要保證是線程安全的

其它

  • Android 7.0 (API level 24) 開始禁止使用 file URI 進行文件共享
  • Android 7.1.1 (API level 25) 開始安裝 APK 時必須聲明 REQUEST_INSTALL_PACKAGES 權限,數據必須經過 FileProvider 形式共享,數據類型是 application/vnd.android.package-archive,必須給 Intent 添加 FLAG_GRANT_READ_URI_PERMISSION flag 授予臨時訪問權限
Intent installIntent = new Intent(Intent.ACTION_VIEW);
File apkPath = new File(Environment.getExternalStorageDirectory(), "apks");
File apkFile = new File(apkPath, "myapp.apk");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    Uri contentUri = FileProvider.getUriForFile(MainActivity.this, "com.example.myapp.fileprovider", apkFile);
    installIntent.setDataAndType(contentUri, "application/vnd.android.package-archive");
    installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} else {
    installIntent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
}
installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (installIntent.resolveActivity(getPackageManager()) != null) {
    startActivity(installIntent);
}
複製代碼

Notification 相關

  • Android 5.0 (API level 21) 開始通知能夠出如今鎖屏頁面
  • Android 7.0 (API level 24) 開始能夠在通知中直接輸入文本或執行一些自定義操做,如直接回覆按鈕
  • Android 8.0 (API level 26) 開始全部的通知必須屬於一個 channel,channel 被用戶看做是 categories,即通知類別,用戶經過通知類別來精確管理各個應用或一個應用內的通知。一個應用能夠有多個通知類別,如私信類別、好友請求類別、應用更新類別等等。能夠給每一個通知類別指定通知的 importance,即重要程度,Urgent(緊急)會發出提示音並在屏幕上彈出通知,High(高)會發出提示音,Medium(中)不發出提示音,Low(低)不發出提示音而且不會出如今狀態欄中。在 Android 8.0 (API level 26) 如下的系統中通知的重要程度表現爲 priority,即優先級。對應關係分別爲: IMPORTANCE_HIGH 對應 PRIORITY_HIGHPRIORITY_MAXIMPORTANCE_DEFAULT 對應 PRIORITY_DEFAULTIMPORTANCE_LOW 對應 PRIORITY_LOWIMPORTANCE_MIN 對應 PRIORITY_MIN。在應用啓動時能夠執行下面的代碼建立通知類別,能夠無反作用地屢次執行
private void createNotificationChannel() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        CharSequence name = getString(R.string.channel_name);
        String description = getString(R.string.channel_description);
        int importance = NotificationManager.IMPORTANCE_DEFAULT;
        NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
        channel.setDescription(description);
        NotificationManager notificationManager = getSystemService(NotificationManager.class);
        notificationManager.createNotificationChannel(channel);
    }
}
複製代碼
  • 經過 NotificationChannelenableLights()setLightColor() 等方法能夠指定該通知類別默認的通知行爲,可是一旦建立了應用就不能再對它作任何更改了,只有用戶本身能夠更改設置。能夠經過 Intent 引導用戶跳轉至對應設置頁
Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
intent.putExtra(Settings.EXTRA_CHANNEL_ID, myNotificationChannel.getId());
startActivity(intent);
複製代碼
  • 查詢用戶當前的通知類別的設置能夠經過 getNotificationChannel()getNotificationChannels()getVibrationPattern()getImportance() 等方法獲取
  • 使用 deleteNotificationChannel(id) 能夠刪除通知類別,可是在開發模式下可能須要重裝應用或者清除數據纔會徹底刪除
  • 通知類別也能夠分組
// The id of the group.
String groupId = "my_group_01";
// The user-visible name of the group.
CharSequence groupName = getString(R.string.group_name);
NotificationManager mNotificationManager =
        (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager.createNotificationChannelGroup(new NotificationChannelGroup(groupId, groupName));
複製代碼
  • Android 5.0 (API level 21) 開始支持勿擾模式(Do Not Disturb)以禁止任何通知產生的聲音和震動。Total silence(徹底阻止)會阻止包括鬧鐘視頻遊戲在內的全部聲音和震動,Alarms only(僅限鬧鐘)會阻止除了鬧鐘外的全部聲音和震動,Priority only(自訂)能夠定製要屏蔽的信息通話等系統範圍內的通知。setCategory() 方法能夠設置所屬的系統範圍的勿擾類別
  • 每一個通知類別能夠選擇是否覆蓋勿擾模式的設置,當勿擾模式設置爲「僅限優先事項」時,能夠容許繼續接收此類通知
  • Android 8.1 (API level 27) 開始每秒最多播放一次通知提示音,若是一秒內有多個通知那麼只播放一秒內的第一個通知提示音,若是一秒內屢次頻繁更新一個通知,那麼系統可能會丟棄一些通知更新
  • 最好使用 NotificationCompatNotificationManagerCompat 等兼容庫中的類以便方便地適配低版本系統
  • setSmallIcon() 方法能夠設置小圖標,應用名和時間是由系統設置的,setLargeIcon() 方法能夠設置右邊大圖標,setContentTitle()setContentText() 方法能夠設置通知的標題和內容,setPriority() 方法能夠爲 Android 8.0 (API level 26) 如下的系統設置通知優先級。系統範圍的預約義通知類別包括 NotificationCompat.CATEGORY_ALARMNotificationCompat.CATEGORY_REMINDER 等類別,這個類別在勿擾模式中有用,能夠經過 setCategory() 方法指定所屬的系統範圍通知類別
  • 默認的通知內容會收縮成一行,能夠經過 setStyle() 方法設置其餘可展開的通知樣式,.setStyle(new NotificationCompat.BigTextStyle().bigText(emailObject.getSubjectAndSnippet())) 能夠設置大文本塊樣式。.setStyle(new NotificationCompat.InboxStyle().addLine(messageSnippet1) 能夠設置多行的 inbox 樣式。.setStyle(new NotificationCompat.MessagingStyle(resources.getString(R.string.reply_name)).addMessage(message1)) 能夠設置消息樣式,可是此樣式會忽略 setContentTitle()setContentText() 方法的設置,但能夠經過 setConversationTitle() 方法設置該聊天所屬的羣組名。setStyle(new android.support.v4.media.app.Notification.MediaStyle().setShowActionsInCompactView(1).setMediaSession(mMediaSession.getSessionToken())) 能夠設置媒體樣式的通知,屬於 CATEGORY_TRANSPORT 類別。
  • 通知的點擊事件能夠經過 setContentIntent() 方法設置 PendingIntent 對象完成,setAutoCancel(true) 能夠在點擊後自動移除通知
Intent intent = new Intent(this, AlertDetails.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, CHANNEL_ID)
        .setSmallIcon(R.drawable.notification_icon)
        .setContentTitle("My notification")
        .setContentText("Hello World!")
        .setLargeIcon(myBitmap)
        .setStyle(new NotificationCompat.BigPictureStyle()
                .bigPicture(myBitmap)
                .bigLargeIcon(null))
        .setPriority(NotificationCompat.PRIORITY_DEFAULT)
        .setContentIntent(pendingIntent)
        .setAutoCancel(true);
複製代碼
  • 經過 NotificationManagerCompat#notify() 方法能夠顯示通知,你須要定義一個惟一的 int 值的 ID 做爲這個通知的 ID,保存這個 ID 以便以後更新或移除這個通知
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(notificationId, mBuilder.build());
複製代碼
  • 經過 addAction() 方法能夠添加操做按鈕
  • 添加回覆按鈕的流程爲:
private static final String KEY_TEXT_REPLY = "key_text_reply";

String replyLabel = getResources().getString(R.string.reply_label);
RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY)
        .setLabel(replyLabel)
        .build();

PendingIntent replyPendingIntent =
        PendingIntent.getBroadcast(getApplicationContext(),
                conversation.getConversationId(),
                getMessageReplyIntent(conversation.getConversationId()),
                PendingIntent.FLAG_UPDATE_CURRENT);

NotificationCompat.Action action =
        new NotificationCompat.Action.Builder(R.drawable.ic_reply_icon,
                getString(R.string.label), replyPendingIntent)
                .addRemoteInput(remoteInput)
                .build();

Notification newMessageNotification = new Notification.Builder(mContext, CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_message)
        .setContentTitle(getString(R.string.title))
        .setContentText(getString(R.string.content))
        .addAction(action)
        .build();

NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(notificationId, newMessageNotification);
複製代碼
private CharSequence getMessageText(Intent intent) {
    // 在 BroadcastReceiver 接收的 Intent 中能夠根據以前的 KEY 拿到文本框的內容
    Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
    if (remoteInput != null) {
        return remoteInput.getCharSequence(KEY_TEXT_REPLY);
    }
    return null;
 }
複製代碼
// 在回覆完成後更新通知表示已經處理此次回覆,也能夠調用 setRemoteInputHistory() 方法附加回復的內容
Notification repliedNotification = new Notification.Builder(context, CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_message)
        .setContentText(getString(R.string.replied))
        .build();
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(notificationId, repliedNotification);
複製代碼
  • 經過 setProgress(PROGRESS_MAX, PROGRESS_CURRENT, false) 能夠給通知添加肯定進度條,經過 setProgress(0, 0, true) 能夠添加不肯定進度條,經過 setProgress(0, 0, false) 能夠在完成後移除進度條
  • setVisibility() 方法能夠設置鎖屏時的通知顯示策略,VISIBILITY_PUBLIC(顯示全部通知)表示完整地顯示通知內容,VISIBILITY_SECRET(徹底不顯示內容)表示不顯示通知的任何信息,VISIBILITY_PRIVATE(隱藏敏感通知內容)表示只顯示圖標標題等基本信息
  • NotificationManagerCompat#notify() 方法傳遞以前的通知 ID 能夠更新以前的通知,調用 setOnlyAlertOnce() 方法以便只在第一次出現通知時提示用戶
  • 用戶能夠主動清除通知,建立通知時調用 setAutoCancel() 方法能夠在用戶點擊通知後清除通知,建立通知時調用 setTimeoutAfter() 方法能夠在超時後由系統自動清除通知,能夠隨時調用 cancel()cancelAll() 方法清除以前的通知
  • 點擊通知後啓動的 Activity 分爲兩種,一種是應用的正經常使用戶體驗流中的常規 Activity,擁有任務完整的返回棧。還有一種是僅僅用來展現通知的詳細內容的特殊Activity,它不須要返回棧。
  • 對於常規 Activity 須要先經過 android:parentActivityName 屬性或者 android.support.PARENT_ACTIVITY<meta-data> 標籤指定層級關係,而後
Intent resultIntent = new Intent(this, ResultActivity.class);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addNextIntentWithParentStack(resultIntent);
PendingIntent resultPendingIntent =
        stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
複製代碼
  • 對於特殊 Activity 須要先指定 android:taskAffinity=""android:excludeFromRecents="true" 以免在以前的任務中啓動,而後
Intent notifyIntent = new Intent(this, ResultActivity.class);
notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent notifyPendingIntent = PendingIntent.getActivity(
        this, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT
);
複製代碼
  • Android 7.0 (API level 24) 開始,若是一個應用同時有 4 個及以上的通知,那麼系統會自動將它們合併成一組,應用也能夠本身定義和組織分組,用戶點擊後能夠展開成一些單獨的通知,老版本能夠考慮使用 inbox 樣式代替。每一個通知能夠經過 setGroup() 方法指定所屬分組,經過 setSortKey() 方法排序,經過 setGroupAlertBehavior() 指定通知行爲,默認是 GROUP_ALERT_ALL 表示組內全部的通知均可能產生聲音和震動。系統默認會自動生成通知組的摘要,你也能夠單首創建一個表示通知組摘要的通知
int SUMMARY_ID = 0;
String GROUP_KEY_WORK_EMAIL = "com.android.example.WORK_EMAIL";

Notification newMessageNotification1 =
    new NotificationCompat.Builder(MainActivity.this, CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_notify_email_status)
        .setContentTitle(emailObject1.getSummary())
        .setContentText("You will not believe...")
        .setGroup(GROUP_KEY_WORK_EMAIL)
        .build();

Notification newMessageNotification2 =
    new NotificationCompat.Builder(MainActivity.this, CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_notify_email_status)
        .setContentTitle(emailObject2.getSummary())
        .setContentText("Please join us to celebrate the...")
        .setGroup(GROUP_KEY_WORK_EMAIL)
        .build();

Notification summaryNotification =
    new NotificationCompat.Builder(MainActivity.this, CHANNEL_ID)
        .setContentTitle(emailObject.getSummary())
        //set content text to support devices running API level < 24
        .setContentText("Two new messages")
        .setSmallIcon(R.drawable.ic_notify_summary_status)
        //build summary info into InboxStyle template
        .setStyle(new NotificationCompat.InboxStyle()
                .addLine("Alex Faarborg Check this out")
                .addLine("Jeff Chang Launch Party")
                .setBigContentTitle("2 new messages")
                .setSummaryText("janedoe@example.com"))
        //specify which group this notification belongs to
        .setGroup(GROUP_KEY_WORK_EMAIL)
        //set this notification as the summary for the group
        .setGroupSummary(true)
        .build();

NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(emailNotificationId1, newMessageNotification1);
notificationManager.notify(emailNotificationId2, newMessageNotification2);
notificationManager.notify(SUMMARY_ID, summaryNotification);
複製代碼
  • Android 8.0 (API level 26) 開始應用啓動圖標能夠自動添加一個小圓點表示有新的通知,用戶長按應用啓動圖標能夠查看和處理通知,調用 mChannel.setShowBadge(false) 能夠禁用小圓點標誌,調用 setNumber(messageCount) 能夠設置長按後顯示給用戶的消息數,調用 setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL) 能夠設置長按後的圖標樣式,經過 setShortcutId() 能夠隱藏重複的 shortcut
  • 自定義通知內容的樣式須要 setStyle(new NotificationCompat.DecoratedCustomViewStyle()) 樣式,而後調用 setCustomContentView()setCustomBigContentView() 方法指定自定義的摺疊和展開佈局(通常摺疊佈局限制高度爲 64 dp,展開佈局高度限制爲 256 dp),佈局中的控件要使用兼容庫的樣式,如 style="@style/TextAppearance.Compat.Notification.Title"。若是不想使用標準通知模板,不調用 setStyle() 只調用 setCustomBigContentView() 便可
  • Android 5.0 (API level 21) 開始支持頂部彈出的 heads-up 通知,可能觸發 heads-up 通知的條件有: 用戶的 Activity 正處於全屏模式並使用了 fullScreenIntent;Android 8.0 (API level 26) 及更高的設備上通知的重要程度爲 IMPORTANCE_HIGH;Android 8.0 (API level 26) 如下的設備上通知的優先級爲 PRIORITY_HIGHPRIORITY_MAX 而且開啓了鈴聲或震動

Service 相關

  • Service 運行在所在進程的主線程中,它不會自動建立線程,若是你不指定進程的話它甚至不會建立額外的進程,因此若是要執行耗時操做的話應該建立一個新的線程去執行
  • Service 有三種類型,一種是 Foreground service,用來執行用戶能從通知欄感知到的後臺操做,一種是 Background service,是用戶感知不到的,可是 Android 8.0 (API level 26) 開始系統會對這類 Service 進行各類限制,一種是 Bound service,是經過 bindService() 方法綁定到其餘組件的服務,Bound service 基於 C/S 架構的思想,被綁定的組件能夠向這個服務發請求、接收響應數據、甚至 IPC 交互,只要有一個組件綁定了它,他就會立刻運行,多個組件能夠同時綁定它,當全部都解綁時,它就會被銷燬
  • 使用 startService() 方法啓動 Service 時會致使它收到 onStartCommand() 回調,服務會一直運行直到你主動調用它的 stopSelf() 方法或其它組件主動調用 stopService() 方法。若是它只是 Bound service 能夠不是實現該方法
  • 使用 bindService() 方法啓動 Service 時不會回調 onStartCommand() 方法,而會回調 onBind() 方法,爲了與它的 client 通訊這個方法須要返回 IBinder,若是你不容許它綁定就返回空,解綁使用 unbindService() 方法
  • 一個 Service 能夠啓動屢次,可是隻能中止一次,stopSelf(int) 方法能夠在知足指定的 startId 時中止
  • ServiceActivity 同樣其所在的的進程可能隨時被系統殺掉,一樣須要作好銷燬重建的工做
  • IntentService 做爲 Service 的子類能夠方便地在工做線程中完成多個任務,多個任務是一個接一個的執行,因此不會存在線程安全問題,內部是藉助一個 HandlerThread 實現異步處理的,當全部請求都完成後會自動銷燬,onBind() 方法返回了空,爲了方便調試能夠在構造器中指定工做線程的名字,若是想重寫 onCreate()onStartCommand()onDestroy() 方法的實現必須調用父類的實現,IntentService 是在一個工做線程中完成多個任務,因此若是想在多個線程中完成多個任務能夠直接繼承 Service 並藉助 HandlerThread 等線程技術實現
  • 相對於 IntentService 更推薦使用 JobIntentServiceJobIntentService 藉助了 JobSchedulerAsyncTask 完成更靈活的任務調度和處理,只須要申請好 WAKE_LOCK 權限 JobScheduler 就能夠完成 WakeLock 的管理,使用 enqueueWork(Context, Class, int, Intent) 靜態方法提交任務就可讓 onHandleWork(Intent) 回調中的代碼被更好地調度執行了
  • 不能綁定的 Service 只能經過 PendingIntent 進行組件間的通訊
  • Foreground service 的通知欄通知只能經過中止服務或者從前臺移除來解除
  • Android 9 (API level 28) 開始 Foreground service 必須請求 FOREGROUND_SERVICE 權限
  • 使用 startForeground() 方法能夠向系統請求以 Foreground service 模式運行,stopForeground() 能夠請求退出該模式

後臺任務

  • 每一個進程都有一個主線程用來完成任務,通常主線程結束了那麼意味着整個任務完成了,進程就會自動結束退出了
  • Android 應用的主線程用來進行測量繪製 UI、協調用戶操做、接收生命週期事件等工做,是與用戶的感知直接關聯的,因此一般也被叫作 UI 線程,若是在這個線程中作太多工做,那麼會致使這個線程掛起或者卡頓,致使糟糕的用戶體驗。因此像解碼 bitmap、讀寫磁盤、執行網絡請求等須要長時間計算和處理的操做都應該放到單獨的後臺線程中去作
  • 後臺線程雖然是用戶感受不到的,但一般倒是最消耗系統資源的,有的線程大部分時間都在佔用 CPU 完成複雜的計算,咱們管這種稱爲 CPU 密集型操做,有的線程大部分時間都在進行 I/O 的讀寫操做,咱們管這種叫作 I/O 密集型操做。咱們能夠根據不一樣的操做類型選擇不一樣的策略來處理以便最大化系統的吞吐量同時最小化所需代價。同時長時間運行的後臺線程也加重了電量的消耗,因此無論是操做系統仍是開發者都須要 對這些後臺線程的行爲進行限制
  • 在建立一個後臺任務以前,咱們須要先要對它分析一下,它是要立刻執行仍是能夠延遲執行?它須要系統知足指定條件才能執行嗎?它須要在精確的時間點執行嗎?

WorkManager

  • 經過 WorkManager 能夠優雅地執行 可延遲執行的 異步任務,當應用退出後仍然能夠繼續執行,當知足系統條件(聯網、充電、重啓)時仍然能夠觸發任務的執行
  • 特別適合用來向後臺發送日誌或分析數據,或者用來週期性的與服務器同步數據
  • WorkManager 在 Android 6.0 (API level 23) 及以上系統上藉助 JobScheduler 實現,在以前的系統上藉助 BroadcastReceiverAlarmManager 實現
  • WorkManager 能夠對任務添加網絡條件和充電狀態等條件限制,能夠調度一次性的或週期性的任務,能夠監聽和管理被調度的任務,能夠將多個任務連在一塊兒
  • 一次性的任務可使用 OneTimeWorkRequest,週期性的任務使用 PeriodicTimeWorkRequest
  • 若是指定了多個限制,那麼只有在全部限制都知足的狀況下任務纔會執行:
Constraints constraints = new Constraints.Builder()
    .setRequiresDeviceIdle(true)
    .setRequiresCharging(true)
     .build();
OneTimeWorkRequest compressionWork =
                new OneTimeWorkRequest.Builder(CompressWorker.class)
     .setConstraints(constraints)
     .build();
複製代碼
  • 任務交給系統後可能會立刻被執行,能夠經過 setInitialDelay(10, TimeUnit.MINUTES) 設置一個最小延時
  • 若是須要重試任務能夠在 Worker 中使用 Result.retry() 完成,採用的補償策略默認是 EXPONENTIAL 指數級的,可使用 setBackoffCriteria() 方法調整策略
  • 能夠經過 setInputData() 方法爲任務設置輸入數據,在 Worker 中能夠經過 getInputData() 方法獲取到輸入數據,Result.success()Result.failure() 能夠攜帶輸出數據。數據應該儘量的簡單,不能超過 10KB
  • addTag 方法能夠給任務打 Tag,而後就可使用 WorkManager.cancelAllWorkByTag(String)WorkManager.getWorkInfosByTagLiveData(String) 等方法方便操做任務了
  • 若是一個任務的先決任務沒有完成那麼會被認爲是 BLOCKED
  • 若是任務的限制和定時知足要求那麼會被認爲是 ENQUEUED
  • 若是任務正在執行那麼會被認爲是 RUNNING
  • 若是任務返回了 Result.success() 那麼會被認爲是 SUCCEEDED 態,這是最終態,只有 OneTimeWorkRequest 可能進入這個狀態
  • 若是任務返回了 Result.failure() 那麼會被認爲是 FAILED 態,這是最終態,只有 OneTimeWorkRequest 可能進入這個狀態,全部相關的任務也會被標記爲 FAILED 且不會被執行
  • 顯式取消一個沒終止的 WorkRequest 會被認爲是 CANCELLED 態,全部相關的任務也會被標記爲 CANCELLED 且不會被執行
  • WorkManager.getWorkInfoById(UUID)WorkManager.getWorkInfoByIdLiveData(UUID) 等方法能夠定位想要的任務進行觀察
  • 能夠將任務連在一塊兒:
WorkManager.getInstance()
    .beginWith(Arrays.asList(filter1, filter2, filter3))
    .then(compress)
    .then(upload)
    .enqueue();
複製代碼

Foreground service

對於用戶觸發的必須立刻執行且必須執行完的後臺任務,須要使用 Foreground services 實現,它既告訴系統這個應用正在執行重要的任務不能被殺掉,又經過通知欄告訴用戶有後臺工做正在執行php

AlarmManager

若是任務須要在精確的時間點執行,可使用 AlarmManagerjava

DownloadManager

若是須要執行一個長時間的 HTTP 下載任務,可使用 DownloadManagerDownloadManager 獨立於應用以外,能夠在下載失敗、更改網絡鏈接、系統重啓後進行重試android

public static long downloadApk(String url, String title, String desc) {
    DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
    request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI)
            .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
            .setTitle(title)
            .setDescription(desc)
            .setDestinationInExternalFilesDir(MyApplication.getInstance(), null, "apks")
            .allowScanningByMediaScanner();
    DownloadManager downloadManager = (DownloadManager) MyApplication.getInstance().getSystemService(Context.DOWNLOAD_SERVICE);
    return downloadManager.enqueue(request);
}
複製代碼

小技巧

  • 測試 Deep links:
adb shell am start
    -W -a android.intent.action.VIEW
    -d "example://gizmos" com.example.android
複製代碼
  • 測試 Android App Links:
adb shell am start -a android.intent.action.VIEW
    -c android.intent.category.BROWSABLE
    -d "http://domain.name:optional_port"
複製代碼
  • 應用安裝完 20s 後獲取全部應用的連接處理策略:
adb shell dumpsys package domain-preferred-apps
複製代碼
  • 模擬系統殺掉應用進程:
adb shell am kill com.some.package
複製代碼
  • 將文件導入手機:
adb push com.some.package /sdcard/
複製代碼
  • .nomedia 文件會致使其所在目錄不被 Media Scanner 掃描到

附錄

系統欄適配

/** * 華爲手機劉海屏適配 * * @author frank * @see <a href="https://developer.huawei.com/consumer/cn/devservice/doc/50114">《華爲劉海屏手機安卓O版本適配指導》</a> */
public class HwNotchSizeUtil {

    private static final int FLAG_NOTCH_SUPPORT = 0x00010000;

    /** * 是不是劉海屏手機 * * @param context Context * @return true:劉海屏 false:非劉海屏 */
    public static boolean hasNotchInScreen(Context context) {
        boolean ret = false;
        try {
            ClassLoader cl = context.getClassLoader();
            Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
            Method get = HwNotchSizeUtil.getMethod("hasNotchInScreen");
            ret = (boolean) get.invoke(HwNotchSizeUtil);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ret;
    }

    /** * 獲取劉海尺寸 * * @param context Context * @return int[0]值爲劉海寬度 int[1]值爲劉海高度 */
    public static int[] getNotchSize(Context context) {
        int[] ret = new int[]{0, 0};
        try {
            ClassLoader cl = context.getClassLoader();
            Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
            Method get = HwNotchSizeUtil.getMethod("getNotchSize");
            ret = (int[]) get.invoke(HwNotchSizeUtil);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ret;
    }

    /** * 設置應用窗口在華爲劉海屏手機使用劉海區 * * @param window Window */
    public static void setFullScreenWindowLayoutInDisplayCutout(Window window) {
        if (window == null) {
            return;
        }
        WindowManager.LayoutParams layoutParams = window.getAttributes();
        try {
            Class layoutParamsExCls = Class.forName("com.huawei.android.view.LayoutParamsEx");
            Constructor con = layoutParamsExCls.getConstructor(ViewGroup.LayoutParams.class);
            Object layoutParamsExObj = con.newInstance(layoutParams);
            Method method = layoutParamsExCls.getMethod("addHwFlags", int.class);
            method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /** * 設置應用窗口在華爲劉海屏手機不使用劉海區顯示 * * @param window Window */
    public static void setNotFullScreenWindowLayoutInDisplayCutout(Window window) {
        if (window == null) {
            return;
        }
        WindowManager.LayoutParams layoutParams = window.getAttributes();
        try {
            Class layoutParamsExCls = Class.forName("com.huawei.android.view.LayoutParamsEx");
            Constructor con = layoutParamsExCls.getConstructor(ViewGroup.LayoutParams.class);
            Object layoutParamsExObj = con.newInstance(layoutParams);
            Method method = layoutParamsExCls.getMethod("clearHwFlags", int.class);
            method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
複製代碼
/** * 小米手機劉海屏適配 * * @author frank * @see <a href="https://dev.mi.com/console/doc/detail?pId=1293">《小米劉海屏 Android O 適配》</a> * @see <a href="https://dev.mi.com/console/doc/detail?pId=1341">《小米劉海屏 Android P 適配》</a> */
public class XiaomiNotchSizeUtil {

    private static final int FLAG_NOTCH_OPEN = 0x00000100;
    private static final int FLAG_NOTCH_PORTRAIT = 0x00000200;
    private static final int FLAG_NOTCH_LANDSCAPE = 0x00000400;

    /** * 是不是劉海屏手機 * * @param context Context * @return true:劉海屏 false:非劉海屏 */
    public static boolean hasNotchInScreen(Context context) {
        boolean ret = false;
        try {
            ret = "1".equals(getSystemProperty("ro.miui.notch"));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ret;
    }

    /** * 獲取劉海尺寸 * * @param context Context * @return int[0]值爲劉海寬度 int[1]值爲劉海高度 */
    public static int[] getNotchSize(Context context) {
        int[] ret = new int[]{0, 0};
        try {
            int widthResId = context.getResources().getIdentifier("notch_width", "dimen", "android");
            if (widthResId > 0) {
                ret[0] = context.getResources().getDimensionPixelSize(widthResId);
            }
            int heightResId = context.getResources().getIdentifier("notch_height", "dimen", "android");
            if (heightResId > 0) {
                ret[1] = context.getResources().getDimensionPixelSize(heightResId);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ret;
    }

    /** * 橫豎屏都繪製到耳朵區 * * @param window Window */
    public static void setFullScreenWindowLayoutInDisplayCutout(Window window) {
        if (window == null) {
            return;
        }
        try {
            Method method = Window.class.getMethod("addExtraFlags",
                    int.class);
            method.invoke(window, FLAG_NOTCH_OPEN | FLAG_NOTCH_PORTRAIT | FLAG_NOTCH_LANDSCAPE);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /** * 橫豎屏都不會繪製到耳朵區 * * @param window Window */
    public static void setNotFullScreenWindowLayoutInDisplayCutout(Window window) {
        if (window == null) {
            return;
        }
        try {
            Method method = Window.class.getMethod("clearExtraFlags",
                    int.class);
            method.invoke(window, FLAG_NOTCH_OPEN | FLAG_NOTCH_PORTRAIT | FLAG_NOTCH_LANDSCAPE);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static String getSystemProperty(String key) {
        String ret = null;
        BufferedReader bufferedReader = null;
        try {
            Process process = Runtime.getRuntime().exec("getprop " + key);
            bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            StringBuilder stringBuilder = new StringBuilder();
            while ((line = bufferedReader.readLine()) != null) {
                stringBuilder.append(line);
            }
            ret = stringBuilder.toString();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return ret;
    }

}
複製代碼
/** * OPPO手機劉海屏適配 * * @author frank * @see <a href="https://open.oppomobile.com/wiki/doc#id=10159">《OPPO凹形屏適配說明》</a> */
public class OppoNotchSizeUtil {

    /** * 是不是劉海屏手機 * * @param context Context * @return true:劉海屏 false:非劉海屏 */
    public static boolean hasNotchInScreen(Context context) {
        return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
    }

}
複製代碼
/** * VIVO手機劉海屏適配 * * @author frank * @see <a href="https://dev.vivo.com.cn/documentCenter/doc/103">《異形屏應用適配指南》</a> */
public class VivoNotchSizeUtil {

    private static final int MASK_NOTCH_IN_SCREEN = 0x00000020;
    private static final int MASK_ROUNDED_IN_SCREEN = 0x00000008;

    /** * 是不是劉海屏手機 * * @param context Context * @return true:劉海屏 false:非劉海屏 */
    public static boolean hasNotchInScreen(Context context) {
        boolean ret = false;
        try {
            ClassLoader cl = context.getClassLoader();
            Class FtFeature = cl.loadClass("android.util.FtFeature");
            Method get = FtFeature.getMethod("isFeatureSupport", int.class);
            ret = (boolean) get.invoke(FtFeature, MASK_NOTCH_IN_SCREEN);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ret;
    }

}
複製代碼
/** * 錘子手機劉海屏適配 * * @author frank * @see <a href="https://resource.smartisan.com/resource/61263ed9599961d1191cc4381943b47a.pdf">《Smartisan 開發者文檔》</a> */
public class SmartisanNotchSizeUtil {

    private static final int MASK_NOTCH_IN_SCREEN = 0x00000001;

    /** * 是不是劉海屏手機 * * @param context Context * @return true:異形屏 false:非異形屏 */
    public static boolean hasNotchInScreen(Context context) {
        boolean ret = false;
        try {
            ClassLoader cl = context.getClassLoader();
            Class DisplayUtilsSmt = cl.loadClass("smartisanos.api.DisplayUtilsSmt");
            Method get = DisplayUtilsSmt.getMethod("isFeatureSupport", int.class);
            ret = (boolean) get.invoke(DisplayUtilsSmt, MASK_NOTCH_IN_SCREEN);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ret;
    }

}
複製代碼

經過相機或相冊選擇一張圖片

findViewById(R.id.chooseImg).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("image/*");
        if (intent.resolveActivity(getPackageManager()) != null) {
            startActivityForResult(intent, REQUEST_IMAGE_GET);
        }
    }
});
findViewById(R.id.takePicture).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
            File photoFile = null;
            try {
                photoFile = createImageFile();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
            if (photoFile != null) {
                Uri photoURI = FileProvider.getUriForFile(MainActivity.this,
                        "com.your.package.fileprovider",
                        photoFile);
                takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
                startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
            }
        }
    }
});

...

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_IMAGE_GET && resultCode == RESULT_OK) {
        Uri fullPhotoUri = data.getData();
        ParcelFileDescriptor descriptor;
        try {
            descriptor = getContentResolver().openFileDescriptor(fullPhotoUri, "r");
            FileDescriptor fd = descriptor.getFileDescriptor();
            Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd);
            imageView.setImageBitmap(bitmap);
            processImage();
        } catch (Exception e) {
            e.printStackTrace();
        }
    } else if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
        int targetW = imageView.getWidth();
        int targetH = imageView.getHeight();
        BitmapFactory.Options bmOptions = new BitmapFactory.Options();
        bmOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(currentPhotoPath, bmOptions);
        int photoW = bmOptions.outWidth;
        int photoH = bmOptions.outHeight;
        int scaleFactor = Math.min(photoW / targetW, photoH / targetH);
        bmOptions.inJustDecodeBounds = false;
        bmOptions.inSampleSize = scaleFactor;
        Bitmap bitmap = BitmapFactory.decodeFile(currentPhotoPath, bmOptions);
        imageView.setImageBitmap(bitmap);
        processImage();
    }
}
private File createImageFile() throws IOException {
    String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
    String imageFileName = "JPEG_" + timeStamp + "_";
    File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
    File image = File.createTempFile(imageFileName, ".jpg", storageDir);
    currentPhotoPath = image.getAbsolutePath();
    return image;
}
複製代碼
<paths>
    <external-path name="my_images" path="Android/data/com.your.package/files/Pictures" />
</paths>
複製代碼
相關文章
相關標籤/搜索