带你梳理一遍 Android 核心知识
熱文導讀|???點擊標題閱讀
互聯網寒冬下,程序員如何突圍提升自己?
Flutter 與 React Native 誰主沉浮?
女面試官:我拉鏈開了你怎么提醒我?
作者:薛定貓的諤
https://juejin.im/post/5c46db4ae51d4503834d8227
超長好文,建議慢慢品用,由于篇幅超越微信限制,略微有點刪減,不影響閱讀。
0、一句話知識點
1. Android 9 (API level 28) 開始廢棄了 Loader API,包括 LoaderManager 和 CursorLoader 等類的使用。推薦使用 ViewModel 和 LiveData 在 Activity 或 Fragment 生命周期中加載數據;
2. Activity 可以通過 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 保持屏幕常亮,這是最推薦、最簡單、最安全的保持屏幕常亮的方法,給 view 添加 android:keepScreenOn="true" 也是一樣的。
這個只在這個 Activity 生命周期內有效,所以大可放心,如果想提前解除常亮,只需要清除這個 flag 即可。
3. WAKE_LOCK 可以阻止系統睡眠,保持 CPU 一直運行,需要 android.permission.WAKE_LOCK 權限。
通過 powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::MyWakelockTag") 創建實例
通過 wakeLock.acquire() 方法請求鎖,通過 wakelock.release() 釋放鎖
4.?WakefulBroadcastReceiver 結合 IntentService 也可以阻止系統睡眠;
5. Android 8.0 (API level 26) 開始支持自適應啟動圖標,自適應啟動圖標必須由前景和背景兩部分組成,尺寸必須都是 108 x 108 dp,其中內部的 72 x 72 dp 用來顯示圖標,靠近四個邊緣的 18 dp 是保留區域,用來進行視覺交互
6. 對于字體大小自適應的 TextView 寬和高都不能是 wrap_content,autoSizeTextType 默認是 none,設置為 uniform 開啟自適應,默認最小 12sp,最大 112sp,粒度 1px。autoSizePresetSizes 屬性可以設置預置的一些大小
7. Android 8.0 (API level 26) 開始支持 XML 自定義字體,兼容庫可以兼容到 Android 4.1 (API level 16),字體文件路徑為 res/font/,使用屬性為 fontFamily,獲取 Typeface 為 getResources().getFont(R.font.myfont);,兼容庫使用 ResourcesCompat.getFont(context, R.font.myfont)
8. Android 9 (API level 28) 支持控件放大鏡功能,Magnifier 的 show() 方法的參數是相對于被放大 View 的左上角的坐標
9. 工程中的 Drawable 資源只能有一個狀態,你不應該手動更改它的任何屬性,否則會影響到其它使用這個 Drawable 資源的地方
10. Android 7.0 (API level 24) 開始支持在 XML 文件中使用自定義 Drawable,公共頂級類使用全限定名作為標簽名即可 <com.myapp.MyDrawable>,公共靜態內部類可以使用 class 屬性 class="com.myapp.MyTopLevelClass$MyDrawable"
11. Android 5.0 (API level 21) 開始支持為 Drawable 設置 tint
12. Android 5.0 (API level 21) 開始支持矢量圖,支持庫可以支持到 Android 2.1 (API level 7+),兼容低版本是需要 Gradle 插件版本大于 2.0+ 時添加 vectorDrawables.useSupportLibrary = true 并使用 VectorDrawableCompat 和 AnimatedVectorDrawableCompat
1、應用資源
1. 添加資源限定符的順序為:?
SIM 卡所屬的國家代碼和移動網代碼 → 語言區域代碼 → 布局方向 → 最小寬度 → 可用寬度 → 可用高度 → 屏幕大不大 → 屏幕長不長 → 屏幕圓不圓 → 屏幕色域寬不寬 → 屏幕支持的動態范圍高不高 → 屏幕方向 → 設備的 UI 模式 → 夜間模式 → 屏幕像素密度 → 觸摸屏類型 → 鍵盤類型 → 主要的文字輸入方式 ?→ 導航鍵是否可用 → 主要的非觸摸導航方式 → 支持的 API level
2. 一個資源目錄的每種資源限定符最多只能出現一次;
3. 必須提供缺省的資源文件;
4. 資源目錄名是大小寫不敏感的;
5. drawable 資源取別名:
<?xml?version="1.0"?encoding="utf-8"?><resources>????<drawable?name="icon">@drawable/icon_ca</drawable></resources>
<resources>
????<drawable?name="icon">@drawable/icon_ca</drawable>
</resources>
6.?布局文件取別名:
<merge>
????<include?layout="@layout/main_ltr"/>
</merge>
只有動畫、菜單、raw 資源 以及 xml/ 目錄中的資源不能使用別名
7. 尋找使用最優資源的流程:
8. 在應用程序運行時,設備的配置可能會發生變化(如屏幕方向變化、切換到多窗口模式,切換了系統語言),默認情況下系統會銷毀重建正在運行的 Activity ,所以應用程序必須保證銷毀重建的過程中用戶的數據和頁面狀態完好無損地恢復。
如果不想系統銷毀重建你的 Activity 只需要在 manifest 文件的 <activity> 標簽的 android:configChanges 屬性中添加你想自己處理的配置更改,多個配置使用 "|" 隔開,此時系統就不會在這些配置更改后銷毀重建你的這個 Activity 而是直接調用它的 onConfigurationChanged() 回調方法,你需要在這個回調中自己處理配置更改后的行為。
9. Activity 的銷毀重建不但發生在設備配置更改后,只要用戶離開了某個 Activity,那么那個 Activity 就隨時可能被系統銷毀。所以銷毀重建是無法避免的,也不應該逃避,而是應該想辦法保存和恢復狀態
10. 由于各種各樣的硬件都能安裝 Android 操作系統,Android 操作系統之間也可能千差萬別,而應用程序的一些功能是與這些軟硬件息息相關的,如拍照應用需要設備必須有攝像頭才能正常工作。
應用可以通過 <uses-feature> 標簽聲明只有滿足這些軟硬件要求的設備才能安裝,通過它的 android:required 屬性設置該要求是不是必須的,程序中可以通過 PackageManager.hasSystemFeature() 方法判斷.
2、動態申請權限
1. Android 6.0 (API level 23) 開始 targetSdkVersion >= 23 的應用必須在運行時動態申請權限
2. 權限請求對話框是操作系統進行管理的,應用無法也不應該干預。
3. 系統對話框描述的是權限組而不是某個具體權限
4. 調用 requestPermissions() 并不意味著系統一定會彈出權限請求對話框,也就是說不能假設調用該方法后就發生了用戶交互,因為如果用戶之前勾選了 “禁止后不再詢問” 或者系統策略禁止應用獲取權限,那么系統會直接拒絕此次權限請求,沒有任何交互
5. 如果某個權限跟應用的主要功能無關,如應用中廣告可能需要位置權限,用戶可能很費解,此時在申請權限之前彈出對話框向用戶解釋為什么需要這個權限是個不錯的選擇。但不要在所有申請權限之前都彈出對話框解釋,因為頻繁地打斷用戶的操作或讓用戶進行選擇容易讓用戶不耐煩
6. Fragment 中的 onRequestPermissionsResult() 方法只有在使用 Fragment#requestPermissions() 方法申請權限時才可能接收到回調,建議將權限放在所屬 Activity 中申請和處理
7. 應用應該盡量少地申請權限,像讓用戶拍一張照片或者選擇一張圖片完全不需要相機權限和外存權限,可以通過隱式 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:?在用戶主動授予權限后重新檢查權限,但不要在這里進行事務提交等生命周期敏感操作
????}
}
注意看上述代碼中TODO標識。
3、Shortcut
1. 類似于 iOS 的 3D Touch,長按啟動圖標彈出幾個快捷入口,入口最好不要超過 4 個,像搜索、掃描二維碼、發帖等應用程序最常用功能的入口被稱為靜態 shortcut,不會隨著用戶不同或隨著用戶使用而改變。
還有一種像從某個存檔點繼續游戲、任務進度等與用戶相關的上下文敏感入口被稱為動態 shortcut,會因用戶不同或隨著用戶使用不斷變化。還有一種在 Android 8.0 (API level 26) 及以上系統版本上像固定網頁標簽等用戶主動固定到桌面的快捷方式被稱為固定 shortcut
2. 靜態 shortcut 系統可以自動備份和恢復,動態 shortcut 需要應用自己備份和恢復,固定 shortcut 的圖標系統無法備份和恢復因此需要應用自己完成
3. android:shortcutId 和 android:shortcutShortLabel 屬性是必須的,android:shortcutShortLabel 不能超過 10 個字符,android:shortcutLongLabel 不能超過 25 個字符,android:icon 不能包含 tint
4. 獲取 ShortcutManager 的方式有兩個: getSystemService(ShortcutManager.class) 和 getSystemService(Context.SHORTCUT_SERVICE)
5. 創建固定 shortcut:
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());
}
4、系統欄適配
1. Android 4.1 (API level 16) 開始可以通過 setSystemUiVisibility() 方法在各個 view 層次中(一般是在 DecorView 中)配置 UI flag 實現系統欄(狀態欄、導航欄統稱)配置,最終匯總體現到 window 級
2.?View.SYSTEM_UI_FLAG_FULLSCREEN 可以隱藏狀態欄,View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 可以隱藏導航欄。但是: 用戶的任何交互包括觸摸屏幕都會導致 flag 被清除進而系統欄保持可見,一旦離開當前 Activity flag 就會被清除,所以如果在 onCreate() 方法中設置了這個 flag 那么按 HOME 鍵再回來狀態欄又保持可見了。
非要這樣設置的話一般要放在 onResume() ?或 onWindowFocusChanged() 方法中,而且這樣設置只有在目標 View 可見時才會生效,狀態欄/導航欄的顯示隱藏會導致顯示內容的大小尺寸跟著變化。
3. 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) 方法)完成
4. lean back 全屏模式: View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION,隱藏狀態欄和導航欄,任何交互都會清除 flag 使系統欄保持可見
5. Immersive 全屏模式: View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION,隱藏狀態欄和導航欄,從被隱藏的系統欄邊緣向內滑動會使系統欄保持可見,應用無法響應這個手勢
6. sticky immersive 全屏模式: View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION,隱藏狀態欄和導航欄,從被隱藏的系統欄邊緣向內滑動會使系統欄暫時可見,flag 不會被清除,且系統欄的背景是半透明的,會覆蓋應用的內容,應用也可以響應這個手勢,在用戶沒有任何交互或者沒有系統欄交互幾秒鐘后系統欄會自動隱藏
7. 真正的沉浸式全屏體驗需要 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
8. 監聽系統欄可見性(sticky immersive 全屏模式無法監聽):
????@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
????????}
????}
});
9. 全面屏適配只需要指定支持的最大寬高比即可:?
<meta-data android:name="android.max_aspect" android:value="2.4"/>
10. 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 指定默認的劉海區渲染模式
11. 華為手機通過 <meta-data android:name="android.notch_support" android:value="true" /> 屬性聲明應用是否已經適配了劉海屏,如果沒適配,那么在橫屏或者豎屏不顯示狀態欄時會禁止渲染到劉海區,可以參考: 《華為劉海屏手機安卓O版本適配指導》。
12. 小米手機通過 <meta-data android:name="notch.config" android:value="portrait|landscape" /> 設置默認的劉海區渲染模式,開發者文檔:?
小米劉海屏 Android O 適配
https://dev.mi.com/console/doc/detail?pId=1293
小米劉海屏 Android P 適配?
https://dev.mi.com/console/doc/detail?pId=1341
13. 其他手機的開發者文檔有: OPPO 手機的?
OPPO凹形屏適配說明
https://open.oppomobile.com/wiki/doc#id=10159
VIVO 手機的?
異形屏應用適配指南?https://dev.vivo.com.cn/documentCenter/doc/103
錘子手機的
Smartisan 開發者文檔https://resource.smartisan.com/resource/61263ed9599961d1191cc4381943b47a.pdf
14. Android 5.0 (API level 21) 開始支持通過 window 的 setStatusBarColor() 方法設置狀態欄背景色,要求 window 必須添加 WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 的 flag 并且清除 WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS 的 flag
15. 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
16. 小米手機在 MIUI 開發版 7.7.13 之前需要通過反射兼容亮色背景的狀態欄,開發者文檔:《MIUI 9 & 10“狀態欄黑色字符”實現方法變更通知》
https://dev.mi.com/console/doc/detail?pId=1159
17. 魅族手機同樣需要通過反射兼容亮色背景的狀態欄,開發者文檔:?
《狀態欄變色》
http://open-wiki.flyme.cn/doc-wiki/index#id?79
5、動畫
view 動畫系統只能作用于 view 對象,只能改變 view 的部分樣式,只是簡單改變了 view 繪制,并沒有改變 view 真正的位置和屬性。核心類是 android.view.animation.Animation 和它的 ScaleAnimation 等子類,一般使用 AnimationUtils.loadAnimation() 方法加載。不建議使用,除非為了方便又能滿足現在和將來的需求
1. 屬性動畫系統是一個健壯的、優雅的動畫系統,可以對任意對象的屬性做動畫。核心類是 android.animation.Animator 的子類 ValueAnimator、ObjectAnimator、AnimatorSet
2. 通過調用 ValueAnimator 的 ofInt()、ofFloat() 等工廠方法獲取 ValueAnimator 對象,通過它的 addUpdateListener() 方法可以監聽動畫值并在里面進行自定義操作
3. ObjectAnimator 作為 ValueAnimator 的子類可以自動地為目標對象的命名屬性設置動畫,但是對目標對象有嚴格的要求: 目標對象必須有對應屬性的 setter 方法,如果在工廠方法中只提供了一個動畫值那么它會作為終止值,起始值為目標對象的當前值,此時為了獲取當前屬性值目標對象必須有對應屬性的 getter 方法。有些屬性的更改不會導致 view 重新渲染,此時需要主動調用 invalidate() 方法強制觸發重繪
4. AnimatorListenerAdapter 提供了 Animator.AnimatorListener 接口的空實現
5. 多數情況下可以直接使用系統提供的幾個動畫 duration,如 getResources().getInteger(android.R.integer.config_shortAnimTime)
6. 可以調用任意 view 對象的 animate() 方法獲取 ViewPropertyAnimator 對象,鏈式調用這個對象的 scaleX()、alpha() 等方法可以簡單方便地同時對 view 的多個屬性做動畫
7. 為了更好地重用和管理屬性動畫,最好使用 XML 文件來描述動畫并放到 res/animator/ 目錄下,ValueAnimator 對應 <animator> ,ObjectAnimator 對應 <objectAnimator>,AnimatorSet 對應 <set>,使用 AnimatorInflater.loadAnimator() 可以加載這些動畫
8. 動態 Drawable 的實現有兩種,最傳統最簡單的就是像電影關鍵幀一樣依次指定關鍵幀和每一幀的停留時間,AnimationDrawable 對應于 XML 文件中的 <animation-list>,保存目錄為 res/drawable/,AnimationDrawable 的 start() 方法可以在 onStart() 中調用。還有一種是 AnimatedVectorDrawable,需要 res/drawable/ 中的 <animated-vector> 引用 res/drawable/ 中的 <vector> 對其使用 res/animator/ 中的 <objectAnimator> 動畫
9. 突然更改顯示的內容會讓視覺感受非常突兀不和諧,而且可能意識不到哪些內容突然變了,所以很多場景下需要使用動畫過渡一下,而不是突然更改顯示的內容
10. 顯示隱藏 view 的常用動畫有三個: crossfade 動畫,card flip 動畫,circular reveal 動畫
11. crossfade 動畫就是內容淡出另一個內容淡入交叉進行,也被稱為溶入動畫。實現方式為: 事先將淡入 view 的 visibility 設置為 GONE → 開始動畫時將淡入 view 的 alpha 設置為 0,visibility 設置為 VISIBLE → 將淡入 view 的 alpha 動畫到 1,將淡出 view 的 alpha 動畫到 0 并在動畫結束時將淡出 view 的 visibility 設置為 GONE
12. card flip 動畫就是卡片翻轉動畫,需要四個動畫描述: card_flip_right_in、card_flip_right_out、card_flip_left_in、card_flip_left_out
13. Android 5.0 (API level 21) 開始支持 circular reveal 圓形裁剪動畫,實現方式為: 事先將 view 的 visibility 設置為 INVISIBLE → 利用 ViewAnimationUtils.createCircularReveal() 方法創建半徑從 0 到 Math.hypot(cx, cy) 的圓形裁剪動畫 → 將 view 的 visibility 設置為 VISIBLE 然后開啟動畫
14. 直線動畫移動 view 只需要借助 ObjectAnimator.ofFloat() 方法動畫設置 view 的 translationX 或 translationY 屬性即可
15. 曲線動畫移動 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.xml、fast_out_slow_in.xml、linear_out_slow_in.xml 三個基礎的曲線插值器可以直接使用
16. 放大預覽動畫只需要同時動畫更改目標 view 的 X,Y,SCALE_X,SCALE_Y 屬性即可,不過要先計算好兩個 view 最終的位置和初始縮放比
17. Android 提供了預加載的布局改變動畫,可以通過 android:animateLayoutChanges="true" 屬性告訴系統開啟默認動畫,或者通過 LayoutTransition API 設置
18. 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 的內置子類包括 AutoTransition、Fade、ChangeBounds,可以在 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 的大小過渡動畫可能有問題
19. 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") 傳遞信息
20. 自定義過渡動畫需要繼承 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 需要更改的元素數
21. 動畫可能會影響性能,必要時可以啟用 Profile GPU Rendering 進行調試
6、BroadcastReceiver 相關
1. Android 9 (API level 28) 開始 NETWORK_STATE_CHANGED_ACTION 廣播不再包含 SSID,BSSID 等信息
2. Android 8.0 (API level 26) 開始限制應用靜態注冊一些非當前應用專屬的隱式廣播的 BroadcastReceiver,免除這項限制的廣播包括 ACTION_LOCKED_BOOT_COMPLETED 等不太可能影響用戶體驗的廣播
3. Android 7.0 (API level 24) 開始不能發送和接收 ACTION_NEW_PICTURE 和 ACTION_NEW_VIDEO 系統廣播,可以通過 JobInfo 和 JobParameters 完成。不能靜態注冊 CONNECTIVITY_ACTION 廣播,如果想在網絡變化時調度任務可以選擇使用 WorkManager,如果只在應用運行期間監聽網絡變化使用 ConnectivityManager 比動態注冊注銷 BroadcastReceiver 更優雅
4. 應該盡量在代碼中動態注冊注銷 BroadcastReceiver
5. onReceive() 方法中不能進行復雜工作否則會導致 ANR,onReceive() 方法一旦執行完,系統可能就認為這個廣播接收器已經沒用了,隨時會殺掉包含這個廣播接收器的進程,包括這個進程啟動的線程。使用 goAsync() 方法可以在 PendingResult#finish() 方法執行前為廣播接收器的存活爭取更多的時間,但最好還是使用 JobScheduler 等方式進行長時間處理工作
6. 使用 sendBroadcast() 方法發的廣播屬于常規廣播,所有能接收這個廣播的廣播接收器接收到廣播的順序是不可控的
7. 使用 sendOrderedBroadcast() 方法發的廣播屬于有序廣播,根據廣播接收器的優先級一個接一個地傳遞這條廣播,相同優先級的順序不可控,廣播接收器可以選擇繼續傳遞給下一個,也可以選擇直接丟掉
8. 使用 LocalBroadcastManager.getInstance(this).sendBroadcast() 方法發的廣播屬于應用進程內的本地廣播,這樣的廣播只有應用自己知道,比系統級的全局廣播更安全更有效率
9. 為了保證廣播的 action 全局唯一,action 的名字最好使用應用的包名作為前綴,最好聲明成靜態字符串常量.
7、數據存儲與共享
分享文件
為了安全地共享文件,分享的文件必須通過 content URI 表示,必須授予這個 content URI 臨時訪問權限。FileProvider 作為 ContentProvider 的特殊子類,它的 getUriForFile() 靜態方法可以為文件生成 content URI。
????<meta-data
????????android:name="android.support.FILE_PROVIDER_PATHS"
????????android:resource="@xml/filepaths"?/>
</provider>
????<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?newFile?=?new?File(imagePath,?"default_image.jpg");
Uri?contentUri?=?FileProvider.getUriForFile(getContext(),?"com.example.myapp.fileprovider",?newFile);
1. 給 Intent 添加 FLAG_GRANT_READ_URI_PERMISSION 或 FLAG_GRANT_WRITE_URI_PERMISSION 的 flag 授予對這個 content URI 的臨時訪問權限,該權限會被目標 Activity 所在應用的其它組件繼承,會在所在的任務結束時自動撤銷授權
2. 調用 Context.grantUriPermission(package, Uri, mode_flags) 方法也可以授予 FLAG_GRANT_READ_URI_PERMISSION 或 FLAG_GRANT_WRITE_URI_PERMISSION 權限,但只有在主動調用 revokeUriPermission() 方法后或者重啟系統后才會撤銷授權
????????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
1. ContentProvider 的數據形式和關系型數據庫的表格數據類似,因此 API 也像數據庫一樣包含增刪改查(CRUD)操作,但為了更好地組織管理一個或多個 ContentProvider,最好通過 ContentResolver 操作 ContentProvider
2. 對于 ContentProvider 的增刪改查操作,不能直接在 UI 線程上執行
3. Uri 和 ContentUris 類的靜態方法可以方便地構造 content URI
mCursor?=?getContentResolver().query(
????????UserDictionary.Words.CONTENT_URI,
????????mProjection,
????????mSelectionClause,
????????mSelectionArgs,
????????mSortOrder);
3. 為了防止 SQL 注入,禁止拼接 SQL 語句,如 mSelectionClause 不能直接包含 selectionArgs 參數值
4. ContentProvider 所在應用本身的組件可以隨便訪問它,不需要授權
5. 如果 ContentProvider 的應用不指定任何權限,那么其它應用就無法訪問這個 ContentProvider 的數據
6. 使用者需要事先通過 <uses-permission> 標簽獲取訪問權限
7. 創建 ContentProvider 需要繼承 ContentProvider 并實現增刪改查等一系列方法: onCreate() 在系統創建 provider 后馬上調用,可以在這里創建數據庫,但不要在這里做耗時操作。getType() 返回 content URI 的 MIME 類型。query()、insert()、update()、delete() 進行增刪改查。除了 onCreate() 方法其它方法必須要保證是線程安全的。
8、Notification 相關
1. Android 5.0 (API level 21) 開始通知可以出現在鎖屏頁面
2. Android 7.0 (API level 24) 開始可以在通知中直接輸入文本或執行一些自定義操作,如直接回復按鈕
3. Android 8.0 (API level 26) 開始所有的通知必須屬于一個 channel,channel 被用戶看作是 categories,即通知類別,用戶通過通知類別來精確管理各個應用或一個應用內的通知。
一個應用可以有多個通知類別,如私信類別、好友請求類別、應用更新類別等等。可以給每個通知類別指定通知的 importance,即重要程度,Urgent(緊急)會發出提示音并在屏幕上彈出通知,High(高)會發出提示音,Medium(中)不發出提示音,Low(低)不發出提示音并且不會出現在狀態欄中。
在 Android 8.0 (API level 26) 以下的系統中通知的重要程度表現為 priority,即優先級。
對應關系分別為: IMPORTANCE_HIGH 對應 PRIORITY_HIGH 或 PRIORITY_MAX,IMPORTANCE_DEFAULT 對應 PRIORITY_DEFAULT,IMPORTANCE_LOW 對應 PRIORITY_LOW,IMPORTANCE_MIN 對應 PRIORITY_MIN。在應用啟動時可以執行下面的代碼創建通知類別,可以無副作用地多次執行
????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);
????}
}
4. 通過 NotificationChannel 的 enableLights(),setLightColor() 等方法可以指定該通知類別默認的通知行為,但是一旦創建了應用就不能再對它做任何更改了,只有用戶自己可以更改設置。可以通過 Intent 引導用戶跳轉至對應設置頁
intent.putExtra(Settings.EXTRA_APP_PACKAGE,?getPackageName());
intent.putExtra(Settings.EXTRA_CHANNEL_ID,?myNotificationChannel.getId());
startActivity(intent);
5. 查詢用戶當前的通知類別的設置可以通過 getNotificationChannel()、getNotificationChannels()、getVibrationPattern()、getImportance() 等方法獲取
6. 使用 deleteNotificationChannel(id) 可以刪除通知類別,但是在開發模式下可能需要重裝應用或者清除數據才會完全刪除
7. 通知類別也可以分組
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));
8. Android 5.0 (API level 21) 開始支持勿擾模式(Do Not Disturb)以禁止任何通知產生的聲音和震動。Total silence(完全阻止)會阻止包括鬧鐘視頻游戲在內的所有聲音和震動,Alarms only(僅限鬧鐘)會阻止除了鬧鐘外的所有聲音和震動,Priority only(自訂)可以定制要屏蔽的信息通話等系統范圍內的通知。setCategory() 方法可以設置所屬的系統范圍的勿擾類別
9. 每個通知類別可以選擇是否覆蓋勿擾模式的設置,當勿擾模式設置為“僅限優先事項”時,可以允許繼續接收此類通知
10. Android 8.1 (API level 27) 開始每秒最多播放一次通知提示音,如果一秒內有多個通知那么只播放一秒內的第一個通知提示音,如果一秒內多次頻繁更新一個通知,那么系統可能會丟棄一些通知更新
11. 最好使用 NotificationCompat 和 NotificationManagerCompat 等兼容庫中的類以便方便地適配低版本系統
12. setSmallIcon() 方法可以設置小圖標,應用名和時間是由系統設置的,setLargeIcon() 方法可以設置右邊大圖標,setContentTitle() 和 setContentText() 方法可以設置通知的標題和內容,setPriority() 方法可以為 Android 8.0 (API level 26) 以下的系統設置通知優先級。系統范圍的預定義通知類別包括 NotificationCompat.CATEGORY_ALARM,NotificationCompat.CATEGORY_REMINDER 等類別,這個類別在勿擾模式中有用,可以通過 setCategory() 方法指定所屬的系統范圍通知類別
13. 默認的通知內容會收縮成一行,可以通過 setStyle() 方法設置其他可展開的通知樣式,
可以設置大文本塊樣式。
可以設置多行的 inbox 樣式。
可以設置消息樣式,但是此樣式會忽略 setContentTitle() 和 setContentText() 方法的設置,但可以通過 setConversationTitle() 方法設置該聊天所屬的群組名。
setStyle(new?android.support.v4.media.app.Notification.MediaStyle().setShowActionsInCompactView(1).setMediaSession(mMediaSession.getSessionToken()))?new?android.support.v4.media.app.Notification.MediaStyle().setShowActionsInCompactView(1).setMediaSession(mMediaSession.getSessionToken()))?
可以設置媒體樣式的通知,屬于 CATEGORY_TRANSPORT 類別。
14. 通知的點擊事件可以通過 setContentIntent() 方法設置 PendingIntent 對象完成,setAutoCancel(true) 可以在點擊后自動移除通知
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);
15. 通過 NotificationManagerCompat#notify() 方法可以顯示通知,你需要定義一個唯一的 int 值的 ID 作為這個通知的 ID,保存這個 ID 以便之后更新或移除這個通知:
notificationManager.notify(notificationId,?mBuilder.build());
16. setVisibility() 方法可以設置鎖屏時的通知顯示策略,VISIBILITY_PUBLIC(顯示所有通知)表示完整地顯示通知內容,VISIBILITY_SECRET(完全不顯示內容)表示不顯示通知的任何信息,VISIBILITY_PRIVATE(隱藏敏感通知內容)表示只顯示圖標標題等基本信息
17. NotificationManagerCompat#notify() 方法傳遞之前的通知 ID 可以更新之前的通知,調用 setOnlyAlertOnce() 方法以便只在第一次出現通知時提示用戶
18. 用戶可以主動清除通知,創建通知時調用 setAutoCancel() 方法可以在用戶點擊通知后清除通知,創建通知時調用 setTimeoutAfter() 方法可以在超時后由系統自動清除通知,可以隨時調用 cancel() 或 cancelAll() 方法清除之前的通知
19. 點擊通知后啟動的 Activity 分為兩種,一種是應用的正常用戶體驗流中的常規 Activity,擁有任務完整的返回棧。還有一種是僅僅用來展示通知的詳細內容的特殊Activity,它不需要返回棧。
對于常規 Activity 需要先通過 android:parentActivityName 屬性或者 android.support.PARENT_ACTIVITY 的 <meta-data> 標簽指定層級關系,然后:
TaskStackBuilder?stackBuilder?=?TaskStackBuilder.create(this);
stackBuilder.addNextIntentWithParentStack(resultIntent);
PendingIntent?resultPendingIntent?=
????????stackBuilder.getPendingIntent(0,?PendingIntent.FLAG_UPDATE_CURRENT);
對于特殊 Activity 需要先指定 android:taskAffinity="" 和 android:excludeFromRecents="true" 以避免在之前的任務中啟動,然后:
notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK?|?Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent?notifyPendingIntent?=?PendingIntent.getActivity(
????????this,?0,?notifyIntent,?PendingIntent.FLAG_UPDATE_CURRENT
);
9、后臺任務
1. 每個進程都有一個主線程用來完成任務,一般主線程結束了那么意味著整個任務完成了,進程就會自動結束退出了
2. Android 應用的主線程用來進行測量繪制 UI、協調用戶操作、接收生命周期事件等工作,是與用戶的感知直接關聯的,所以通常也被叫做 UI 線程,如果在這個線程中做太多工作,那么會導致這個線程掛起或者卡頓,導致糟糕的用戶體驗。所以像解碼 bitmap、讀寫磁盤、執行網絡請求等需要長時間計算和處理的操作都應該放到單獨的后臺線程中去做
3. 后臺線程雖然是用戶感覺不到的,但通常卻是最消耗系統資源的,有的線程大部分時間都在占用 CPU 完成復雜的計算,我們管這種稱為 CPU 密集型操作,有的線程大部分時間都在進行 I/O 的讀寫操作,我們管這種叫做 I/O 密集型操作。我們可以根據不同的操作類型選擇不同的策略來處理以便最大化系統的吞吐量同時最小化所需代價。同時長時間運行的后臺線程也加劇了電量的消耗,所以不管是操作系統還是開發者都需要 對這些后臺線程的行為進行限制
4. 在創建一個后臺任務之前,我們需要先要對它分析一下,它是要馬上執行還是可以延遲執行?它需要系統滿足指定條件才能執行嗎?它需要在精確的時間點執行嗎?
WorkManager
1. 通過 WorkManager 可以優雅地執行 可延遲執行的 異步任務,當應用退出后仍然可以繼續執行,當滿足系統條件(聯網、充電、重啟)時仍然可以觸發任務的執行
2. 特別適合用來向后臺發送日志或分析數據,或者用來周期性的與服務器同步數據
3. WorkManager 在 Android 6.0 (API level 23) 及以上系統上借助 JobScheduler 實現,在之前的系統上借助 BroadcastReceiver 和 AlarmManager 實現
4. WorkManager 可以對任務添加網絡條件和充電狀態等條件限制,可以調度一次性的或周期性的任務,可以監聽和管理被調度的任務,可以將多個任務連在一起
5. 一次性的任務可以使用 OneTimeWorkRequest,周期性的任務使用 PeriodicTimeWorkRequest
6. 如果指定了多個限制,那么只有在所有限制都滿足的情況下任務才會執行:
????.setRequiresDeviceIdle(true)
????.setRequiresCharging(true)
?????.build();
OneTimeWorkRequest?compressionWork?=
????????????????new?OneTimeWorkRequest.Builder(CompressWorker.class)
?????.setConstraints(constraints)
?????.build();
7. 任務交給系統后可能會馬上被執行,可以通過 setInitialDelay(10, TimeUnit.MINUTES) 設置一個最小延時
8. 如果需要重試任務可以在 Worker 中使用 Result.retry() 完成,采用的補償策略默認是 EXPONENTIAL 指數級的,可以使用 setBackoffCriteria() 方法調整策略
9. 可以通過 setInputData() 方法為任務設置輸入數據,在 Worker 中可以通過 getInputData() 方法獲取到輸入數據,Result.success() 和 Result.failure() 可以攜帶輸出數據。數據應該盡可能的簡單,不能超過 10KB
10. addTag 方法可以給任務打 Tag,然后就可以使用 WorkManager.cancelAllWorkByTag(String) 和 WorkManager.getWorkInfosByTagLiveData(String) 等方法方便操作任務了
11. 如果一個任務的先決任務沒有完成那么會被認為是 BLOCKED 態
12. 如果任務的限制和定時滿足要求那么會被認為是 ENQUEUED 態
13. 如果任務正在執行那么會被認為是 RUNNING 態
14. 如果任務返回了 Result.success() 那么會被認為是 SUCCEEDED 態,這是最終態,只有 OneTimeWorkRequest 可能進入這個狀態
15. 如果任務返回了 Result.failure() 那么會被認為是 FAILED 態,這是最終態,只有 OneTimeWorkRequest 可能進入這個狀態,所有相關的任務也會被標記為 FAILED 且不會被執行
16. 顯式取消一個沒終止的 WorkRequest 會被認為是 CANCELLED 態,所有相關的任務也會被標記為 CANCELLED 且不會被執行
17. WorkManager.getWorkInfoById(UUID) 和 WorkManager.getWorkInfoByIdLiveData(UUID) 等方法可以定位想要的任務進行觀察
18. 可以將任務連在一起:
????.beginWith(Arrays.asList(filter1,?filter2,?filter3))
????.then(compress)
????.then(upload)
????.enqueue();
Foreground service
對于用戶觸發的必須馬上執行且必須執行完的后臺任務,需要使用 Foreground services 實現,它既告訴系統這個應用正在執行重要的任務不能被殺掉,又通過通知欄告訴用戶有后臺工作正在執行.
AlarmManager
如果任務需要在精確的時間點執行,可以使用 AlarmManager
DownloadManager
如果需要執行一個長時間的 HTTP 下載任務,可以使用 DownloadManager。
DownloadManager 獨立于應用之外,可以在下載失敗、更改網絡連接、系統重啟后進行重試
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);}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);
}
更多學習和討論,歡迎加入我們的知識星球,這里有1000+小伙伴,讓你的學習不寂寞~·
看完本文有收獲?請轉發分享給更多人
我們的知識星球第三期開期了,已達到1100人了,能連續做三期已很不容易了,有很多老用戶續期,目前續期率達到50%,說明了大家對我們的知識星球還是很認可的,歡迎大家加入盡早我們的知識星球,更多星球信息參見:
歡迎加入Java和Android架構社群
如何進階成為Java的Android版和架構師?
說兩件事
微信掃描或者點擊上方二維碼領取的Android \ Python的\ AI \的Java等高級進階資源
更多學習資料點擊下面的“閱讀原文?”獲取
謝謝老板,點個好看↓
總結
以上是生活随笔為你收集整理的带你梳理一遍 Android 核心知识的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 华为交换机打补丁
- 下一篇: mysql高效查询_mysql 高效率查