Android面试复习资料整理
Activity鞏固和復習
1. 什么是Activity
四大組件之一,通常一個用戶交互界面對應一個activity。activity是Context的子類,同時實現了window.callback和keyevent.callback,可以處理與窗體用戶交互的事件。
開發中常用的有FragmentActivity、ListActivity、TabActivity(Android 4.0被Fragment取代)
2. Activity的4種狀態
- running:用戶可以點擊,activity處于棧頂狀態。
- paused:activity失去焦點的時候,被一個非全屏的activity占據或者被一個透明的activity覆蓋,這個狀態的activity并沒有銷毀,它所有的狀態信息和成員變量仍然存在,只是不能夠被點擊。(除了內存緊張的情況,這個activity有可能被回收)
- stopped:這個activity被另外一個activity完全覆蓋,但是這個activity的所有狀態信息和成員變量仍然存在(除了內存緊張)
- killed:這個activity已經被銷毀,其所有的狀態信息和成員變量已經不存在了。
3. Activity生命周期
生命周期的基本介紹
- onCreate:當Activity第一次啟動調用
- onDestroy:當Activity銷毀的時候調用
- onStart:當Activity變成可見調用
- onStop:當Activity不可見調用
- onResume:當Activity可以交互調用這個方法 當界面上的按鈕被點擊的時候調用
- onPause:當Activity不可以交互調用這個方法 當界面上的按鈕不可以點擊
- onRestart:當界面重新啟動的時候調用
生命周期流程
- Activity啟動:調用的依次順序是:onCreate —> onStart —> onResume —> onPause —> onStop —> onDestroy,還有一個onRestart,其中onRestart是在Activity被onStop后,但是沒有被onDestroy,在再次啟動此Activity時調用的(而不再調用onCreate)方法;如果被onDestroy了,則是調用onCreate方法。
- 點擊Home鍵回到主界面(Activity不可見):onPause —> onStop
- 當我們再次回到原Activity時: onRestart —> onStart —> onResume
- 退出當前Activity時: onPause —> onStop —> onDestroy
4. Activity任務棧
- 有序地管理Activity的先進后出的一種數據結構
- 安全退出:任務棧中所有的Activity都出棧
5. Activity的啟動模式
-
standard 標準模式:
特點:此模式不管有沒有已存在的實例,都生成新的實例。每次調用startActivity()啟動Activity時都會創建一個新的Activity放在棧頂,每次返回都會銷毀實例并出棧,可以重復創建。
-
singleTop 單一頂部模式/棧頂復用模式:
特點:會檢查任務棧棧頂的Activity,如果發現棧頂已經存在實例,就不會創建新的實例,直接復用,此時會調用onNewIntent。但如果不在棧頂,那么還是會創建新的實例。
應用場景:瀏覽器書簽的頁面,流氓的網站,避免創建過多的書簽頁面 -
singleTask 單一任務模式/棧內復用模式:
特點:這種模式不會檢查任務棧的棧頂,檢查當前任務棧,如果發現有實例存在,直接復用。任務棧中只有一個實例存儲(把當前activity上面的所有的其它activity都清空,復用這個已經存在的activity)
應用場景:瀏覽器瀏覽頁面的Activity,播放器播放的activity。 -
singleInstance 單一實例模式(用得比較少)
特點:系統會為這個Activity單獨創建一個任務棧,這個任務棧里面只有一個實例存在并且保證不再有其它activity實例進入。
應用場景:來電頁面。
6. Scheme跳轉協議
概念
Android中的scheme是一種頁面內跳轉協議,是一種非常好的實現機制,通過定義自己的scheme協議,可以非常方便跳轉app中的各個頁面;通過scheme協議,服務器可以定制化告訴app跳轉哪個頁面,可以通過通知欄消息定制化跳轉頁面,可以通過H5頁面跳轉頁面等。
應用場景
- 通過服務器下發跳轉路徑跳轉相應頁面
- 通過在H5頁面的錨點跳轉相應的頁面
- 根據服務器下發通知欄消息,App跳轉相應的頁面(包括另外一個APP的頁面,作為推廣使用)
7. 參考文章
Android面試(一):Activity面試你所需知道的一切
android-Scheme與網頁跳轉原生的三種方式
Fragment
1. 什么是Fragment
Fragment,俗稱碎片,自Android 3.0開始被引進并大量使用。作為Activity界面的一部分,Fragment的存在必須依附于Activity,并且與Activity一樣,擁有自己的生命周期,同時處理用戶的交互動作。同一個Activity可以有一個或多個Fragment作為界面內容,并且可以動態添加、刪除Fragment,靈活控制UI內容,也可以用來解決部分屏幕適配問題。
2. Fragment為什么被稱為第五大組件
首先Fragment的使用次數是不輸于其他四大組件的,而且Fragment有自己的生命周期,比Activity更加節省內存,切換模式也更加舒適,使用頻率不低于四大組件。
3. Fragment的生命周期

4. Fragment創建/加載到Activity的兩種方式
-
靜態加載
- 創建Fragment的xml布局文件
- 在Fragment的onCreateView中inflate布局,返回
- 在Activity的布局文件中的適當位置添加fragment標簽,指定name為Fragment的完整類名(這時候Activity中可以直接通過findViewById找到Fragment中的控件)
-
動態加載(需要用到事務操作,常用)
-
創建Fragment的xml布局文件
-
在Fragment的onCreateView中inflate布局,返回
@Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {return inflater.inflate(R.layout.activity_main, container, false); } -
在Activity中通過獲取FragmentManager(SupportFragmentManager),通過beginTransaction()方法開啟事務
-
進行add()/remove()/replace()/attach()/detach()/hide()/addToBackStack()事務操作(都是對Fragment的棧進行操作,其中add()指定的tag參數可以方便以后通過findFragmentByTag()找到這個Fragment)
-
提交事務:commit()
示例代碼:
@Override protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, new TestFragment(), "test").commit();TestFragment f = (TestFragment) getSupportFragmentManager().findFragmentByTag("test"); }
5. Fragment通信問題
通過findFragmentByTag或者getActivity獲得對方的引用(強轉)之后,再相互調用對方的public方法。
優點:簡單粗暴
缺點:引入了“強轉”的丑陋代碼,另外兩個類之間各自持有對方的強引用,耦合較大,容易造成內存泄漏
通過Bundle的方法進行傳值,在添加Fragment的時候進行通信
@Override protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);Fragment fragment = new TestFragment();Bundle bundle = new Bundle();bundle.putString("key", "value");//Activity中對fragment設置一些參數fragment.setArguments(bundle);getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, fragment, "test").commit(); }優點:簡單粗暴
缺點:只能在Fragment添加到Activity的時候才能使用,屬于單向通信
利用eventbus進行通信
優點:實時性高,雙向通信,Activity與Fragment之間可以完全解耦
缺點:反射影響性能,無法獲取返回數據,EventBUS難以維護
利用接口回調進行通信(Google官方推薦)
//MainActivity實現MainFragment開放的接口 public class MainActivity extends FragmentActivity implements FragmentListener {@overridepublic void toH5Page() {//...其他處理代碼省略} } //Fragment的實現 public class MainFragment extends Fragment {//接口的實例,在onAttach Activity的時候進行設置public FragmentListener mListener;//MainFragment開放的接口public static interface FragmentListener {//跳到h5頁面void toH5Page();}@Overridepublic void onAttach(Activity activity) {super.onAttach(activity);//對傳遞進來的Activity進行接口轉換if (activity instance FragmentListener){mListener = ((FragmentListener) activity);}}...其他處理代碼省略 }優點:既能達到復用,又能達到很好的可維護性,并且性能得到保證
缺點:假如項目很大了,Activity與Fragment的數量也會增加,這時候為每對Activity與Fragment交互定義交互接口就是一個很麻煩的問題(包括為接口的命名,新定義的接口相應的Activity還得實現,相應的Fragment還得進行強制轉換)
通過Handler進行通信(其實就是把接口的方式改為Handler)
優點:既能達到復用,又能達到很好的可維護性,并且性能得到保證
缺點:Fragment對具體的Activity存在耦合,不利于Fragment復用和維護,沒法獲取Activity的返回數據
通過廣播/本地廣播進行通信
優點:簡單粗暴
缺點:大材小用,存在性能損耗,傳播數據必須實現序列化接口
父子Fragment之間通信,可以使用getParentFragment()/getChildFragmentManager()的方式進行
6. FragmentPageAdapter和FragmentPageStateAdapter的區別
-
FragmentPageAdapter在每次切換頁面的時候,是將Fragment進行分離,適合頁面較少的Fragment使用以保存一些內存,對系統內存不會多大影響
@Override public void destroyItem(ViewGroup container, int position, Object object) {if (mCurTransaction == null) {mCurTransaction = mFragmentManager.beginTransaction();}if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object+ " v=" + ((Fragment)object).getView());//FragmentPageAdapter在destroyItem的時候調用detachmCurTransaction.detach((Fragment)object); } -
FragmentPageStateAdapter在每次切換頁面的時候,是將Fragment進行回收,適合頁面較多的Fragment使用,這樣就不會消耗更多的內存
@Override public void destroyItem(ViewGroup container, int position, Object object) {Fragment fragment = (Fragment) object;if (mCurTransaction == null) {mCurTransaction = mFragmentManager.beginTransaction();}if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object+ " v=" + ((Fragment)object).getView());while (mSavedState.size() <= position) {mSavedState.add(null);}mSavedState.set(position, fragment.isAdded()? mFragmentManager.saveFragmentInstanceState(fragment) : null);mFragments.set(position, null);//FragmentPageStateAdapter在destroyItem的時候調用removemCurTransaction.remove(fragment); }
7. 參考文章
Android:Activity與Fragment通信(99%)完美解決方案
Service
1. 什么是Service
Service是四大組件之一,它可以在后臺執行長時間運行操作而沒有用戶界面的應用組件
2. Service的兩種啟動方式與生命周期
startService特點:
bindService特點:
3. Service和Thread的區別
- Service是安卓中系統的組件,它運行在獨立進程的主線程中,默認情況下不可以執行耗時操作(否則ANR)
- Thread是程序執行的最小單元,分配CPU的基本單位,可以開啟子線程執行耗時操作
- Service在不同Activity中可以獲取自身實例,可以方便的對Service進行操作
- Thread的運行是獨立于Activity的,也就是說當一個Activity被finish之后,如果沒有主動停止Thread或者Thread里的run方法沒有執行完畢的話,Thread也會一直執行,引發內存泄漏;另一方面,沒有辦法在不同的Activity中對同一Thread進行控制。
Broadcast
1. BroadcastReceiver是什么
BroadcastReceiver是四大組件之一,是一種廣泛運用在應用程序之間傳輸信息的機制,通過發送Intent來傳送我們的數據。
2. BroadcastReceiver的使用場景
- 不同組件之間的消息通信(應用內/應用內不同進程/不同進程(應用))
- 與Android系統在特定情況下的通信(如電話呼入、藍牙狀態變化等)
- 線程之間的通信
3. Broadcast種類
普通廣播(Normal Broadcast)
- 通過sendBroadcast進行發送,如果注冊了Action匹配的接受者則會收到
- 若發送廣播有相應權限,那么廣播接收者也需要相應權限
系統廣播(System Broadcast)
- Android中內置了多個系統廣播:只要涉及到手機的基本操作(如開機、網絡狀態變化、拍照等等),都會發出相應的廣播
- 每個廣播都有特定的Intent - Filter(包括具體的action)
- 系統廣播由系統發送,不需要手動發送,只需要注冊監聽
有序廣播(Ordered Broadcast)
- 通過sendOrderedBroadcast發送
- 發送出去的廣播被廣播接收者按照先后順序接收(有序是針對廣播接收者而言的)
- 廣播接受者接收廣播的順序規則:Priority大的優先;動態注冊的接收者優先
- 先接收的可以對廣播進行截斷和修改
App應用內廣播(本地廣播、Local Broadcast)
- 通過LocalBroadcastManager.getInstance(this).sendBroadcastSync();
- App應用內廣播可理解為一種局部廣播,廣播的發送者和接收者都同屬于一個App
- 相比于全局廣播(普通廣播),App應用內廣播優勢體現在:安全性高 & 效率高(本地廣播只會在APP內傳播,安全性高;不允許其他APP對自己的APP發送廣播,效率高)
粘性廣播(Sticky Broadcast)
- 在Android5.0 & API 21中已經失效,所以不建議使用
- 通過sendStickyBroadcast發送
- 粘性廣播在發送后就一直存在于系統的消息容器里面,等待對應的處理器去處理,如果暫時沒有處理器處理這個廣播則一直在消息容器里面處于等待狀態
- 粘性廣播的Receiver如果被銷毀,那么下次重新創建的時候會自動接收到消息數據
4. 廣播的注冊方式
- 靜態注冊:也稱為清單注冊,就是在AndroidManifest.xml中注冊的廣播。此類廣播接收器在應用尚未啟動的時候就可以接收到相應廣播。
- 動態注冊:也稱為運行時注冊,也就是在Service或者Activity組件中,通過Context.registerReceiver()注冊廣播接收器。此類廣播接收器是在應用已啟動后,通過代碼進行注冊。生命周期與組件一致。
5. 廣播的實現機制
6. 本地廣播的使用以及實現機制
-
基本使用:可以通過intent.setPackage(packageName)指定包名,也可以使用localBroadcastManager(常用),示例代碼如下:
//注冊應用內廣播接收器 //步驟1:實例化BroadcastReceiver子類 & IntentFilter mBroadcastReceiver mBroadcastReceiver = new mBroadcastReceiver(); IntentFilter intentFilter = new IntentFilter();//步驟2:實例化LocalBroadcastManager的實例 localBroadcastManager = LocalBroadcastManager.getInstance(this);//步驟3:設置接收廣播的類型 intentFilter.addAction(android.net.conn.CONNECTIVITY_CHANGE);//步驟4:調用LocalBroadcastManager單一實例的registerReceiver()方法進行動態注冊 localBroadcastManager.registerReceiver(mBroadcastReceiver, intentFilter);//取消注冊應用內廣播接收器 localBroadcastManager.unregisterReceiver(mBroadcastReceiver);//發送應用內廣播 Intent intent = new Intent(); intent.setAction(BROADCAST_ACTION); localBroadcastManager.sendBroadcast(intent); -
localBroadcastManager的實現機制
- LocalBroadcastManager高效的原因主要是因為它內部是通過Handler實現的,它的sendBroadcast()方法含義和我們平時所用的全局廣播不一樣,它的sendBroadcast()方法其實是通過handler發送一個Message實現的。
- 既然是它內部是通過Handler來實現廣播的發送的,那么相比與系統廣播通過Binder實現那肯定是更高效了,同時使用Handler來實現,別的應用無法向我們的應用發送該廣播,而我們應用內發送的廣播也不會離開我們的應用
- LocalBroadcastManager內部協作主要是靠這兩個Map集合:mReceivers和mActions,當然還有一個List集合mPendingBroadcasts,這個主要就是存儲待接收的廣播對象
7. 參考文章
Android四大組件:BroadcastReceiver史上最全面解析
Android 粘性廣播StickyBroadcast的使用
咦,Oreo怎么收不到廣播了?
LocalBroadcastManager—創建更高效、更安全的廣播
WebView
1. WebView遠程代碼執行安全漏洞
漏洞描述
Android API level 16以及之前的版本存在遠程代碼執行安全漏洞,該漏洞源于程序沒有正確限制使用WebView.addJavascriptInterface方法,遠程攻擊者可通過使用Java Reflection API利用該漏洞執行任意Java對象的方法。
簡單的說就是通過addJavascriptInterface給WebView加入一個JavaScript橋接接口,JavaScript通過調用這個接口可以直接操作本地的JAVA接口。
示例代碼
WebView代碼如下所示:
mWebView = new WebView(this); mWebView.getSettings().setJavaScriptEnabled(true); mWebView.addJavascriptInterface(this, "injectedObj"); mWebView.loadUrl("file:///android_asset/www/index.html");發送惡意短信:
<html><body><script>var objSmsManager = injectedObj.getClass().forName("android.telephony.SmsManager").getM ethod("getDefault",null).invoke(null,null);objSmsManager.sendTextMessage("10086",null,"this message is sent by JS when webview is loading",null,null);</script></body> </html>利用反射機制調用Android API getRuntime執行shell命令,最終操作用戶的文件系統:
<html><body><script>function execute(cmdArgs){return injectedObj.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs);}var res = execute(["/system/bin/sh", "-c", "ls -al /mnt/sdcard/"]);document.write(getContents(res.getInputStream()));</script></body> </html>漏洞檢測
漏洞修復
允許被調用的函數必須以@JavascriptInterface進行注解(API Level小于17的應用也會受影響)
建議不要使用addJavascriptInterface接口,以免帶來不必要的安全隱患,采用動態地生成將注入的JS代碼的方式來代替
如果一定要使用addJavascriptInterface接口:
移除Android系統內部的默認內置接口
removeJavascriptInterface("searchBoxJavaBridge_"); removeJavascriptInterface("accessibility"); removeJavascriptInterface("accessibilityTraversal");2. JSBridge
客戶端和服務端之間可以通過JSBridge來互相調用各自的方法,實現雙向通信
3. WebView的正確銷毀與內存泄漏問題
由于WebView是依附于Activity的,Activity的生命周期和WebView啟動的線程的生命周期是不一致的,這會導致WebView一直持有對這個Activity的引用而無法釋放,解決方案如下
獨立進程,簡單暴力,不過可能涉及到進程間通信(推薦)
動態添加WebView,對傳入WebView中使用的Context使用弱引用
正確銷毀WebView,WebView在其他容器上時(如:LinearLayout),當銷毀Activity時,需要:
4. WebView后臺耗電
問題
在WebView加載頁面的時候,會自動開啟線程去加載,如果不很好的關閉這些線程,就會導致電量消耗加大。
解決方法
可以采用暴力的方法,直接在onDestroy方法中System.exit(0)結束當前正在運行中的java虛擬機
5. WebView硬件加速
WebView硬件加速以及缺點
Android3.0引入硬件加速,默認會開啟,WebView在硬件加速的情況下滑動更加平滑,性能更加好,但是會出現白塊或者頁面閃爍的副作用。
解決方案
建議在需要的地方WebView暫時關閉硬件加速
6. WebViewClient的onPageFinished問題
問題
WebViewClient.onPageFinished在每次頁面加載完成的時候調用,但是遇到未加載完成的頁面跳轉其他頁面時,就會被一直調用
解決方案
使用WebChromeClient.onProgressChanged替代WebViewClient.onPageFinished
7. 參考文章
WebView 遠程代碼執行漏洞淺析
Android WebView遠程執行代碼漏洞淺析
Android WebView 遠程代碼執行漏洞簡析
在WebView中如何讓JS與Java安全地互相調用
Android系統架構與Framework源碼分析
1. Android系統架構
根據上圖,Android系統架構從上往下分別是:
2. Android Framework源碼分析
寫給Android App開發人員看的Android底層知識(1)- Binder與AIDL
寫給Android App開發人員看的Android底層知識(2)- AMS與APP、Activity的啟動流程
寫給Android App開發人員看的Android底層知識(3)- AMS與APP、Activity的啟動流程
寫給Android App開發人員看的Android底層知識(4)- Context
寫給Android App開發人員看的Android底層知識(5)- Service
寫給Android App開發人員看的Android底層知識(6)- BroadcastReceiver
寫給Android App開發人員看的Android底層知識(7)- ContentProvider
寫給Android App開發人員看的Android底層知識(8)- PMS及App安裝過程
除此之外,還有消息機制、窗口管理等源碼分析,推薦《開發藝術探索》,以及LooperJing的文集:
Android源碼解析
- 備注:源碼分析部分先放一放,后續補充一些簡要概括性的
消息機制與Handler
1. 基本概念
Android的消息機制主要包括Handler、MessageQueue和Looper。
Handler是Android中引入的一種讓開發者參與處理線程中消息循環的機制。每個Handler都關聯了一個線程,每個線程內部都維護了一個消息隊列MessageQueue,這樣Handler實際上也就關聯了一個消息隊列。可以通過Handler將Message和Runnable對象發送到該Handler所關聯線程的MessageQueue(消息隊列)中,然后該消息隊列一直在循環拿出一個Message,對其進行處理,處理完之后拿出下一個Message,繼續進行處理,周而復始。
2. 為什么要有消息機制
Android的UI控件不是線程安全的,如果在多線程中訪問UI控件則會導致不可預期的狀態。那為什么不對UI控件訪問加鎖呢?
訪問加鎖缺點有兩個:
那我們不用線程來操作不就行了嗎?但這是不可能的,因為Android的主線程不能執行耗時操作,否則會出現ANR。
所以,從各方面來說,Android消息機制是為了解決在子線程中無法訪問UI的矛盾。
3. Handler的工作原理
如圖所示,在主線程ActivityThread中的main方法入口中,先是創建了系統的Handler(H),創建主線程的Looper,將Looper與主線程綁定,調用了Looper的loop方法之后開啟整個應用程序的主循環。Looper里面有一個消息隊列,通過Handler發送消息到消息隊列里面,然后通過Looper不斷去循環取出消息,交給Handler去處理。通過系統的Handler,或者說Android的消息處理機制就確保了整個Android系統有條不紊地運作,這是Android系統里面的一個比較重要的機制。
我們的APP也可以創建自己的Handler,可以是在主線程里面創建,也可以在子線程里面創建,但是需要手動創建子線程的Looper并且手動啟動消息循環。
4. Handler的內存泄漏問題
原因
非靜態內部類持有外部類的匿名引用,導致Activity無法釋放(生命周期不一致)
解決方案
- Handler內部持有外部Activity的弱引用
- Handler改為靜態內部類
- 在適當時機移除Handler的所有Callback()
5. 為什么在子線程中創建Handler會拋異常?
Handler的工作是依賴于Looper的,而Looper(與消息隊列)又是屬于某一個線程(ThreadLocal是線程內部的數據存儲類,通過它可以在指定線程中存儲數據,其他線程則無法獲取到),其他線程不能訪問。
因此Handler就是間接跟線程是綁定在一起了。因此要使用Handler必須要保證Handler所創建的線程中有Looper對象并且啟動循環。因為子線程中默認是沒有Looper的,所以會報錯。
正確的在子線程中創建Handler的方法如下(可以使用HandlerThread代替):
handler = null;new Thread(new Runnable() {private Looper mLooper;@Overridepublic void run() {//必須調用Looper的prepare方法為當前線程創建一個Looper對象,然后啟動循環//prepare方法中實質是給ThreadLocal對象創建了一個Looper對象//如果當前線程已經創建過Looper對象了,那么會報錯Looper.prepare();handler = new Handler();//獲取Looper對象mLooper = Looper.myLooper();//啟動消息循環Looper.loop();//在適當的時候退出Looper的消息循環,防止內存泄漏mLooper.quit();}}).start();注意:
- 主線程中默認是創建了Looper并且啟動了消息的循環的,因此不會報錯。
- 應用程序的入口是ActivityThread的main方法,在這個方法里面會創建Looper,并且執行Looper的loop方法來啟動消息的循環,使得應用程序一直運行。
- 有時候出于業務需要,主線程可以向子線程發送消息。子線程的Handler必須按照上述方法創建,并且關聯Looper。
6. 為什么不能在子線程更新UI?
UI更新的時候,會對當前線程進行檢驗,如果不是主線程,則拋出異常:
void checkThread() {if (mThread != Thread.currentThread()) {throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views.");} }比較特殊的三種情況:
- 在Activity創建完成后(Activity的onResume之前ViewRootImpl實例沒有建立),mThread被賦值為主線程(ViewRootImpl),所以直接在onCreate中創建子線程是可以更新UI的
- 在子線程中添加 Window,并且創建 ViewRootImpl,可以在子線程中更新view
- SurfaceView可以在其他線程更新
7. 參考文章
Android 源碼分析之旅3.1–消息機制源碼分析
android消息機制原理詳解
Android中Handler的使用
AsyncTask
1. AsyncTask的基本概念與基本工作原理
它本質上就是一個封裝了線程池和Handler的異步框架。
AsyncTask執行任務時,內部會創建一個進程作用域的線程池來管理要運行的任務,也就是說當你調用了AsyncTask.execute()后,AsyncTask會把任務交給線程池,由線程池來管理創建Thread和運行Thread。
2. AsyncTask使用方法
三個參數
- Params:表示后臺任務執行時的參數類型,該參數會傳給AysncTask的doInBackground()方法
- Progress:表示后臺任務的執行進度的參數類型,該參數會作為onProgressUpdate()方法的參數
- Result:表示后臺任務的返回結果的參數類型,該參數會作為onPostExecute()方法的參數
五個方法
- onPreExecute():異步任務開啟之前回調,在主線程中執行
- doInBackground():執行異步任務,在線程池中執行
- onProgressUpdate():當doInBackground中調用publishProgress時回調,在主線程中執行
- onPostExecute():在異步任務執行之后回調,在主線程中執行
- onCancelled():在異步任務被取消時回調
3. AsyncTask的版本差異
內部的線程池的版本差異
串行、并行的版本差異
4. AsyncTask的缺陷
內存泄漏問題
原因
非靜態內部類持有外部類的匿名引用,導致Activity無法釋放(生命周期不一致,與Handler一樣)
解決方案
- AsyncTask內部持有外部Activity的弱引用
- AsyncTask改為靜態內部類
- 在Activity銷毀之前,調用AsyncTask.cancel()取消AsyncTask的運行,以此來保證程序的穩定
結果丟失問題
原因
在屏幕旋轉、Activity在內存緊張時被回收等造成Activity重新創建時AsyncTask數據丟失的問題。當Activity銷毀并重新創建后,還在運行的AsyncTask會持有一個Activity的非法引用即之前的Activity實例。導致onPostExecute()沒有任何作用(一般是對UI更新無效)。
解決方案
5. 參考文章
AsyncTask 使用和缺陷
HandlerThread
1. HandlerThread產生背景
重點(防止線程多次創建、銷毀):當系統有多個耗時任務需要執行時,每個任務都會開啟一個新線程去執行耗時任務,這樣會導致系統多次創建和銷毀線程,從而影響性能。為了解決這一問題,Google提供了HandlerThread,HandlerThread是在線程中創建一個Looper循環器,讓Looper輪詢消息隊列,當有耗時任務進入隊列時,則不需要開啟新線程,在原有的線程中執行耗時任務即可,否則線程阻塞。
HandlerThread集Thread和Handler之所長,適用于會長時間在后臺運行,并且間隔時間內(或適當情況下)會調用的情況,比如上面所說的實時更新。
2. HandlerThread的特點
-
HandlerThread本質上是一個線程,繼承自Thread,與線程池不同,HandlerThread是一個串行隊列,背后只有一個線程
-
HandlerThread有自己的Looper對象,可以進行Looper循環,可以創建Handler
public class HandlerThread extends Thread {Looper mLooper;private @Nullable Handler mHandler; } -
HandlerThread可以在Handler的handleMessage中執行異步方法,異步不會堵塞,減少對性能的消耗
-
HandlerThread缺點是不能同時繼續進行多任務處理,需要等待進行處理,處理效率較低
IntentService
1. IntentService是什么
- 重點(本質上也是為了節省資源)
- IntentService是繼承自Service并處理異步請求的一個類,其內部采用HandlerThread和Handler實現的,在IntentService內有一個工作線程來處理耗時操作,其優先級比普通Service高
- 當任務完成后,IntentService會自動停止,而不需要手動調用stopSelf()
- 可以多次啟動IntentService,每個耗時操作都會以工作隊列的方式在IntentService中onHandlerIntent()回調方法中執行,并且每次只會執行一個工作線程
2. IntentService使用方法
3. IntentService工作原理
-
IntentService繼承自Service,內部有一個HandlerThread對象
-
在onCreate的時候會創建一個HandlerThread對象,并啟動線程
-
緊接著創建ServiceHandler對象,ServiceHandler繼承自Handler,用來處理消息。ServiceHandler將獲取HandlerThread的Looper就可以開始正常工作了
@Overridepublic void onCreate() {super.onCreate();HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");thread.start();mServiceLooper = thread.getLooper();mServiceHandler = new ServiceHandler(mServiceLooper);} -
每啟動一次onStart方法,就會把數消息和數據發給mServiceHandler,相當于發送了一次Message消息給HandlerThread的消息隊列。
@Overridepublic void onStart(@Nullable Intent intent, int startId) {Message msg = mServiceHandler.obtainMessage();msg.arg1 = startId;msg.obj = intent;mServiceHandler.sendMessage(msg);} -
mServiceHandler會把數據傳給onHandleIntent方法,onHandleIntent是個抽象方法,需要在IntentService實現,所以每次onStart方法之后都會調用我們自己寫的onHandleIntent方法去處理。處理完畢使用stopSelf通知HandlerThread已經處理完畢,HandlerThread繼續觀察消息隊列,如果還有未執行玩的message則繼續執行,否則結束。
private final class ServiceHandler extends Handler {public ServiceHandler(Looper looper) {super(looper);}@Overridepublic void handleMessage(Message msg) {onHandleIntent((Intent)msg.obj);stopSelf(msg.arg1);}}
Android項目構建過程
下圖展示了從一個Android項目構建出一個帶有簽名、對齊操作的APK包的完整過程(省略了代碼混淆過程、NDK的編譯過程):
下面是具體描述:
詳細版本如下:
代碼混淆
1. 代碼混淆及其優點
代碼混淆的過程
混淆其實是包括了代碼壓縮、代碼混淆以及資源壓縮等的優化過程。
這四個流程默認開啟,在Android項目中我們可以選擇將“優化”和“預校驗”關閉:
代碼混淆的優點
代碼混淆的優點如下:
- ProGuard混淆流程將檢測主項目以及依賴庫中未被使用的類、類成員、方法、屬性并移除,這有助于規避64K方法數的瓶頸
- 將類、類成員、方法重命名為無意義的簡短名稱,增加了逆向工程的難度(由于Java是一門跨平臺的解釋性語言,其源代碼被編譯成class字節碼來適應其他平臺,而class文件包含了Java源代碼信息,很容易被反編譯)
- 移除未被使用的資源,可以有效減小apk安裝包大小
2. 代碼混淆操作、調試步驟
開啟混淆、開啟資源壓縮
android {buildTypes {release {minifyEnabled trueshrinkResources trueproguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'}} }在proguard-rules.pro添加自定義混淆規則
-
第三方庫所需的混淆規則。正規的第三方庫一般都會在接入文檔中寫好所需混淆規則,使用時注意添加。
-
在運行時動態改變的代碼,例如反射。比較典型的例子就是會與 json 相互轉換的實體類。假如項目命名規范要求實體類都要放在model包下的話,可以添加類似這樣的代碼把所有實體類都保持住:
-keep public class **.*Model*.** {*;} -
JNI中調用的類。
-
WebView中JavaScript調用的方法
-
Layout布局使用的View構造函數、android:onClick等。
檢查混淆結果,避免因混淆引入的bug。一方面,需要從代碼層面檢查。使用上文的配置進行混淆打包后在 /build/outputs/mapping/release/ 目錄下會輸出以下文件:
- dump.txt:描述APK文件中所有類的內部結構
- mapping.txt:提供混淆前后類、方法、類成員等的對照表
- seeds.txt:列出沒有被混淆的類和成員
- usage.txt:列出被移除的代碼
gi
開啟代碼混淆后的調試
retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]3. 代碼混淆的工作原理
ProGuard過程中將無用的字段或方法存入到EntryPoint中,將非EntryPoint的字段和方法進行替換,其中Entry Point是在ProGuard過程中不會被處理的類或方法。詳細描述如下:
4. 參考文章
寫給Android開發者的混淆使用手冊
持續集成
1. 持續集成的基本概念
- 持續集成(Continuous Integration),持續集成是一種軟件開發實踐,通過自動化的構建(包括編譯、發布和自動化測試)來驗證,從而幫助盡快發現集成錯誤。
- 持續集成一直被認為是敏捷開發的重要實踐之一,也是提升軟件質量的重要手段。特別在團隊協作開發中,為項目添加持續集成還是非常有必要的,確保了任何時間、任何地點生成可部署的軟件。
2. Jenkins+Git+Gradle實現持續集成
3. 參考文章
Android Jenkins+Git+Gradle持續集成-實在太詳細
ANR
1. ANR是什么
Android中,主線程(UI線程)如果在規定時內沒有處理完相應工作,就會出現ANR(Application Not Responding),彈出頁面無響應的對話框。
2. ANR分類
3. ANR的核心原因
- 主線程在做一些耗時的工作
- 主線程被其他線程鎖
- cpu被其他進程占用,該進程沒被分配到足夠的cpu資源。
4. ANR的原理
5. ANR的分析方法(主要是分析是否有死鎖、通過調用棧定位耗時操作、系統資源情況)
從/data/anr/traces.txt中找到ANR反生的信息:可以從log中搜索“ANR in”或“am_anr”,會找到ANR發生的log,該行會包含了ANR的時間、進程、是何種ANR等信息,如果是BroadcastReceiver的ANR可以懷疑BroadCastReceiver.onReceive()的問題,如果的Service或Provider就懷疑是否其onCreate()的問題。
在該條log之后會有CPU usage的信息,表明了CPU在ANR前后的用量(log會表明截取ANR的時間),從各種CPU Usage信息中大概可以分析如下幾點:
- 如果某些進程的CPU占用百分比較高,幾乎占用了所有CPU資源,而發生ANR的進程CPU占用為0%或非常低,則認為CPU資源被占用,進程沒有被分配足夠的資源,從而發生了ANR。這種情況多數可以認為是系統狀態的問題,并不是由本應用造成的。
- 如果發生ANR的進程CPU占用較高,如到了80%或90%以上,則可以懷疑應用內一些代碼不合理消耗掉了CPU資源,如出現了死循環或者后臺有許多線程執行任務等等原因,這就要結合trace和ANR前后的log進一步分析了。
- 如果CPU總用量不高,該進程和其他進程的占用過高,這有一定概率是由于某些主線程的操作就是耗時過長,或者是由于主進程被鎖造成的。
除了上述的情況1以外,分析CPU usage之后,確定問題需要我們進一步分析trace文件。trace文件記錄了發生ANR前后該進程的各個線程的stack。對我們分析ANR問題最有價值的就是其中主線程的stack,一般主線程的trace可能有如下幾種情況:
- 主線程是running或者native而對應的棧對應了我們應用中的函數,則很有可能就是執行該函數時候發生了超時。
- 主線程被block:非常明顯的線程被鎖,這時候可以看是被哪個線程鎖了,可以考慮優化代碼。如果是死鎖問題,就更需要及時解決了。
- 由于抓trace的時刻很有可能耗時操作已經執行完了(ANR -> 耗時操作執行完畢 ->系統抓trace)。
6. 如何避免ANR的方法(常見場景)
主線程避免執行耗時操作(文件操作、IO操作、數據庫操作、網絡訪問等):
Activity、Service(默認情況下)的所有生命周期回調
BroadcastReceiver的onReceive()回調方法
AsyncTask的回調除了doInBackground,其他都是在主線程中
沒有使用子線程Looper的Handler的handlerMessage,post(Runnable)都是執行在主線程中
盡量避免主線程的被鎖的情況,在一些同步的操作主線程有可能被鎖,需要等待其他線程釋放相應鎖才能繼續執行,這樣會有一定的死鎖、從而ANR的風險。對于這種情況有時也可以用異步線程來執行相應的邏輯。
7. 參考文章
Android ANR問題總結
內存管理
1. 內存管理的兩大核心與目標
內存管理的兩大核心
- 內存分配機制
- 內存回收機制
內存管理的目標
- 更少的占用內存,讓更多的進程存活在內存當中
- 在合適的時候,合理的釋放系統資源,保證新的進程能夠被創建合分配內存(注意這里沒有說是立即釋放,因為頻繁的創建釋放會造成內存抖動)
- 在系統內存緊張的時候,能釋放掉大部分不重要的資源
- 能合理的在特殊生命周期中,保存或還原重要數據
2. Android中內存管理機制的特點
-
Android系統是基于Linux 2.6內核開發的開源操作系統,而linux系統的內存管理有其獨特的動態存儲管理機制。
-
Android系統對Linux的內存管理機制進行了優化。
-
Android分配機制上面的優化:
- Android會為每個進程分配一個初始內存大小heapstartsiz(初始分配小內存,使得系統運行更多的進程)
- 當應用需要大內存的時候,繼續分配更多的內存,最大限制為heapgrowthlimit,否則觸發OOM
-
Android內存回收機制上面的優化:
- Linux系統會在進程活動停止后就結束該進程;Android把這些進程都保留在內存中,直到系統需要更多內存為止。這些保留在內存中的進程通常情況下不會影響整體系統的運行速度,并且當用戶再次激活這些進程時,提升了進程的啟動速度。
- Android會根據進程的內存占用、進程優先級等方面,采用LRU算法進行回收
3. 常見的內存問題的相關概念
- 內存溢出:指程序在申請內存時,沒有足夠的空間供其使用
- 內存泄漏:指程序分配出去的內存不再使用,無法進行回收
- 內存抖動:指程序短時間內大量創建對象,然后回收的現象
4. 參考文章
Android內存管理機制
淺談Android內存管理
內存泄漏
1. 什么是內存泄漏
內存泄漏是一個對象已經不需要再使用了,但是因為其它的對象持有該對象的引用,導致它的內存不能被垃圾回收器回收。內存泄漏的慢慢積累,最終會導致OOM的發生。
2. 內存泄漏的主要原因
長生命周期的對象持有短生命周期對象的引用就很可能發生內存泄漏。(短生命周期的對象不能被正確回收)
3. Java內存分配策略
- 靜態存儲區(方法區):主要存儲全局變量和靜態變量,在整個程序運行期間都存在
- 棧區:方法體的局部變量會在棧區創建空間,并在方法執行結束后會自動釋放變量的空間和內存
- 堆區:保存動態產生的數據,如:new出來的對象和數組,在不使用的時候由Java回收器自動回收
4. 常見的內存泄漏及其解決方案
- 單例造成的內存泄漏:在單例中,使用context.getApplicationContext()作為單例的context
- 匿名內部類造成的內存泄漏:由于非靜態內部類持有匿名外部類的引用,必須將內部類設置為static、或者使用弱引用
- Handler造成的內存泄漏:使用static的Handler內部類,同時在實現內部類中持有Context的弱引用
- 避免使用static變量:由于static變量會跟Activity生命周期一致,當Activity退出后臺被后臺回收時,static變量是不安全,所以也要管理好static變量的生命周期
- 資源未關閉造成的內存泄漏:比如Socket、Broadcast、Cursor、Bitmap、ListView、集合容器等,使用完后要關閉
- AsyncTask造成的內存泄漏:由于非靜態內部類持有匿名內部類的引用而造成內存泄漏,可以通過AsyncTask內部持有外部Activity的弱引用同時改為靜態內部類或在onDestroy()中執行AsyncTask.cancel()進行修復
- WebView造成的內存泄漏:頁面銷毀的時候WebView需要正確移除并且調用其destroy方法
5. LeakCanary檢測內存泄漏核心原理
6. 參考文章
常見的內存泄漏原因及解決方法
用 LeakCanary 檢測內存泄漏
InputMethodManager.mLastSrvView memory leak in Android6.0 with huawei mobile phone #572
內存溢出
1. 內存溢出是什么?
OOM指Out of memory(內存溢出),當前占用內存 + 我們申請的內存資源 超過了虛擬機的最大內存限制就會拋出Out of memory異常。
2. 應用的內存限制與申請大內存
-
Android虛擬機對單個應用的最大內存分配值定義在/system/build.prop文件中
//堆分配的初始大小,它會影響到整個系統對RAM的使用程度,和第一次使用應用時的流暢程度。它值越小,系統ram消耗越慢,但一些較大應用一開始不夠用,需要調用gc和堆調整策略,導致應用反應較慢。它值越大,這個值越大系統ram消耗越快,但是應用更流暢。 dalvik.vm.heapstartsize=xxxm //單個應用可用最大內存。最大內存限制主要針對的是這個值,它表示單個進程內存被限定在xxxm,即程序運行過程中實際只能使用xxxm內存,超出就會報OOM。(僅僅針對dalvik堆,不包括native堆) dalvik.vm.heapgrowthlimit=xxxm //單個進程可用的最大內存,但如果存在heapgrowthlimit參數,則以heapgrowthlimit為準。heapsize表示不受控情況下的極限堆,表示單個虛擬機或單個進程可用的最大內存。 dalvik.vm.heapsize=xxxm ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); int memoryClass = am.getMemoryClass(); -
每開一個應用就會打開一個獨立的虛擬機(這樣設計就會在單個程序崩潰的情況下不會導致整個系統的崩潰)
-
在android開發中,如果要使用大堆,需要在manifest中指定android:largeHeap為true,這樣dvm heap最大可達heapsize。盡量少使用large heap。使用額外的內存會影響系統整體的用戶體驗,并且會使得GC的每次運行時間更長。在任務切換時,系統的性能會變得大打折扣。
3. 內存優化-常見內存溢出及其解決方案
- 解決應用中的內存泄漏問題。
- 圖片:正確縮放、壓縮、解碼、回收。
- 在UI不可見的時候,適當釋放其UI資源。
- 列表控件:重用convertView、使用LRU緩存算法、滾動的時候進行監聽,此時不應該加載圖片。
- View:避免在onDraw方法中創建對象。
- 謹慎使用多進程:使用多進程可以把應用中的部分組件運行在單獨的進程當中,這樣可以擴大應用的內存占用范圍,但是這個技術必須謹慎使用,使用多進程會使得代碼邏輯更加復雜,使用不當可能反而會導致顯著增加內存。當應用需要運行一個常駐后臺的任務,而且這個任務并不輕量,可以考慮使用這個技術。
- 使用更加輕量的數據結構。例如,我們可以考慮使用ArrayMap/SparseArray而不是HashMap等傳統數據結構。
- 避免在Android里面使用Enum,而用靜態常量代替。(因為枚舉的內存消耗是靜態常量的兩倍左右)
- 字符串拼接:在有些時候,代碼中會需要使用到大量的字符串拼接的操作,這種時候有必要考慮使用StringBuilder來替代頻繁的“+”。
- Try catch某些大內存分配的操作。
- 資源文件需要選擇合適的文件夾進行存放。hdpi/xhdpi/xxhdpi等等不同dpi的文件夾下的圖片在不同的設備上會經過scale的處理,拉伸之后內存消耗更大。對于不希望被拉伸的圖片,需要放到assets或者nodpi的目錄下。
- 在onLowMemory()與onTrimMemory()中適當釋放內存。
- 珍惜Services資源。如果你的應用需要在后臺使用service,除非它被觸發并執行一個任務,否則其他時候Service都應該是停止狀態。另外需要注意當這個service完成任務之后因為停止service失敗而引起的內存泄漏。建議使用IntentService。
- 優化布局層次,減少內存消耗。越扁平化的視圖布局,占用的內存就越少,效率越高。
- 謹慎使用依賴注入框架:使用之后,代碼是簡化了不少。然而,那些注入框架會通過掃描你的代碼執行許多初始化的操作,這會導致你的代碼需要大量的內存空間來mapping代碼,而且mapped pages會長時間的被保留在內存中。
- 使用ProGuard來剔除不需要的代碼,通過移除不需要的代碼,重命名類,域與方法等等對代碼進行壓縮,優化與混淆。使得代碼更加緊湊,能夠減少mapping代碼所需要的內存空間。
- 謹慎使用第三方libraries。很多開源的library代碼都不是為移動網絡環境而編寫的,如果運用在移動設備上,并不一定適合。即使是針對Android而設計的library,也需要特別謹慎,特別是如果你知道引入的library具體做了什么事情的時候。
- 考慮不同的實現方式、方案、策略來優化內存占用。
4. 參考文章
Android 性能優化(內存之OOM)
Android 查看每個應用的最大可用內存
Lint與代碼優化工具
1. 什么是Lint?
Android Lint是一個靜態代碼分析工具,能夠對檢測代碼質量——對項目中潛在的Bug、可優化的代碼、安全性、性能、可用性、可訪問性、國際化等進行檢查(注:Lint檢測不局限于代碼,功能十分強大)。
通過下面的gradle命令開啟Lint:
./gradlew lint2. Lint的工作流程
-
App項目源文件:包括Java代碼,XML代碼,圖標,以及ProGuard配置文件、Git忽略文件等
-
lint.xml:Lint 檢測的執行標準配置文件,我們可以修改它來允許或者禁止報告一些問題
-
Lint工具按照標準配置文件中指定的規則去檢測App項目源文件,發現問題,并且進行報告。常見的問題有:
- Correctness:不夠完美的編碼,比如硬編碼、使用過時API等
- Performance:對性能有影響的編碼,比如:靜態引用,循環引用等
- Internationalization:國際化,直接使用漢字,沒有使用資源引用等,適配國際化的時候資源漏翻譯
- Security:不安全的編碼,比如在WebView中允許使用JavaScriptInterface等
- …
3. 忽略Lint警告
- Java代碼中忽略Lint警告:使用注解。注解跟@SuppressWarnings很類似,@SuppressLint(“忽略的警告名稱”),如果不清楚警告名稱,直接寫all表示忽略所有警告
- XML代碼中忽略Lint警告:使用 tools:ignore=”忽略的警告名”
4. Debug構建中關閉Lint檢測
執行Gradle命令的時候,通過-x參數不執行某個action
./gradlew build -x lint5. 自定義Lint
-
創建lint.xml到根目錄下,可以自定義Lint安全等級、忽略文件等
-
自定義Lint檢查規則(比如日志不通過LogUtils打印則警告):
- 依賴Android官方的lint的庫
- 創建類繼承Detector,實現一些規則
- 并且提供IssueRegistry向外提供Detector注冊匯總信息
- 輸出jar包或者aar包,主項目進行依賴
- 進行lint檢測即可
6. 一些其他的代碼優化工具
- KW(Klockwork)掃描工具(這個工具需要授權才能使用,屬于動態檢測,依賴項目編譯生成的文件。目前還是存在一些BUG,比如空指針的檢測、IO流的關閉檢測等)
- 阿里巴巴的編碼規約掃描工具(IDEA的一個插件)
- Uber的NullAway空指針掃描工具(空指針的檢測工具,接入比較麻煩)
- ……
7. 參考文章
Android 性能優化:使用 Lint 優化代碼、去除多余資源
Android工具:被你忽視的Lint
自動規避代碼陷阱——自定義Lint規則
Android Lint
美團點評技術團隊-Android自定義Lint實踐2——改進原生Detector
美團自定義Lint示例
Writing a Lint Check
Idea 阿里代碼規約插件安裝
使用Klockwork進行代碼分析簡單操作流程
NullAway:Uber用于檢測Android上的NullPointerExceptions的開源工具
UI卡頓優化
1. UI卡頓原理
View的繪制幀數保持60fps是最佳,這要求每幀的繪制時間不超過16ms(1000/60),如果安卓不能在16ms內完成界面的渲染,那么就會出現卡頓現象。而UI的繪制在主線程中進行的,因此UI卡頓本質上就是主線程卡頓。
2. UI卡頓常見原因及其解決方案
-
布局Layout過于復雜,無法在16ms內完成渲染。
-
過度繪制overDraw,導致像素在同一幀的時間內被繪制多次,使CPU和GPU負載過重。
-
View頻繁的觸發measure、layout,導致measure、layout累計耗時過多和整個View頻繁的重新渲染。
布局優化
- 通過開發者工具檢查過度繪制
- 使用include復用布局、使用ViewStub延遲加載布局、使用merge減少代碼層級、使用RelativeLayout也能大大減少視圖的層級、慎重設置整體背景顏色防止過度繪制
- 使用自定義View取代復雜的View
-
使用TraceView工具檢測UI卡頓、方法耗時
-
在UI線程中做輕微的耗時操作,導致UI線程卡頓:應該把耗時操作放在子線程中進行。
-
同一時間動畫執行的次數過多,導致CPU和GPU負載過重。
-
頻繁的觸發GC操作導致線程暫停、內存抖動,會使得安卓系統在16ms內無法完成繪制:可以考慮使用享元模式、避免在onDraw方法中創建對象等。
-
冗余資源及邏輯等導致加載和執行緩慢。
-
UI卡頓最嚴重的后果是ANR,因此需要在開發中避免和解決ANR問題。
-
列表控件滑動卡頓:復用convertView、滑動不進行加載、使用壓縮圖片、加載縮略圖等。
3. BlockCanary及其原理
BlockCanary會在發生卡頓的時候記錄各種信息,輸出到配置目錄下的文件,并彈出消息欄通知,輕松找出Android App界面卡頓元兇。
BlockCanary的核心原理是:通過Android的消息機制在mainLooperPrinter中判斷start和end,來獲取主線程dispatch該message的開始和結束時間,并判定該時間超過閾值(如2000毫秒)為主線程卡慢發生,并dump出各種信息,提供開發者分析性能瓶頸。
核心代碼如下:
Looper.getMainLooper().setMessageLogging(mainLooperPrinter);@Override public void println(String x) {if (!mStartedPrinting) {mStartTimeMillis = System.currentTimeMillis();mStartThreadTimeMillis = SystemClock.currentThreadTimeMillis();mStartedPrinting = true;} else {final long endTime = System.currentTimeMillis();mStartedPrinting = false;if (isBlock(endTime)) {notifyBlockEvent(endTime);}} }private boolean isBlock(long endTime) {return endTime - mStartTimeMillis > mBlockThresholdMillis; }4. 參考文章
BlockCanary — 輕松找出Android App界面卡頓元兇
冷啟動與熱啟動
1. 冷啟動與熱啟動是什么?
-
冷啟動:當啟動應用時,后臺沒有該應用的進程,這時系統會重新創建一個新的進程分配給該應用,這個啟動方式就是冷啟動。
冷啟動大致流程:實例化Application -> 實例化入口Activity -> 顯示Activity(配置主題中背景等屬性 -> 顯示測量布局繪制最終顯示在界面上)
-
熱啟動:當啟動應用時,后臺已有該應用的進程,所以在已有進程的情況下,這種啟動會從已有的進程中來啟動應用,這個方式叫熱啟動。(例:按back鍵、home鍵,應用雖然會退出,但是該應用的進程是依然會保留在后臺,可進入任務列表查看)
熱啟動大致流程(不需要實例化Application):實例化入口Activity -> 顯示Activity
2. 應用啟動時間測量
-
使用命令
adb shell am start -W [packageName]/[packageName.XXXActivity] -
使用Activity.reportFullyDrawn在Logcat中打印出來,例子:
ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms -
使用TraceView精確測量(TraceView工具可以檢測UI卡頓、方法耗時)
// start tracing to "/sdcard/calc.trace" Debug.startMethodTracing("calc"); // ... // stop tracing Debug.stopMethodTracing(); -
使用高速攝像機進行抓幀
3. 冷啟動優化常見方案
- 通過懶加載方式初始化第三方SDK,可以嘗試在閃屏Activity中加載
- 不要在mainThread中加載資源
- 減少第一個界面onCreate()方法的工作量
- 不要讓Application參與業務的操作、耗時操作
- 不要以靜態變量的方式在Application中保存數據
- 減少布局的復雜性和深度
4. 解決冷啟動白/黑屏問題的兩種方案
-
設置要啟動的Activity主題為透明,可以給用戶造成一個視覺上的假象,例如:
<style name="AppTransparentTheme" parent="Theme.AppCompat.Light.NoActionBar.Fullscreen"><item name="android:windowIsTranslucent">true</item><item name="android:windowBackground">@android:color/transparent</item> </style> -
為要啟動的Activity設置主題為一張背景圖,例如:
<style name="AppBackgroundTheme" parent="Theme.AppCompat.Light.NoActionBar.Fullscreen"><item name="android:windowBackground">@mipmap/bg_welcome</item> </style>
5. 參考文章
Android Study 之冷啟動優化(解決啟動短暫白屏or黑屏)
Android冷啟動實現APP秒開
Android 性能優化 冷啟動速度優化
APK瘦身
1. APK文件的組成
直接在Android Studio中打開APK文件,通過APK分析器,可以看到APK文件的組成成分與比例(實際上是調用AAPT工具的功能):
- asserts:存放一些配置文件或資源文件,比如WebView的本地html,React Native的jsbundle等
- lib:lib目錄下會有各種so文件,分析器會檢查出項目自己的so和各種庫的so。
- resources.arsc:編譯后的二進制資源文件,里面是id-name-value的一個Map。
- res:res目錄存放的是資源文件。包括圖片、字符串。raw文件夾下面是音頻文件,各種xml文件等等。
- dex:dex文件是Java代碼打包后的字節碼,一個dex文件最多只支持65536個方法,開啟了dex分包的話會有多個。
- META-INF:META-INF目錄下存放的是簽名信息,分別是MANIFEST.MF、CERT.SF、CERT.RSA。用來保證apk包的完整性和系統的安全性,幫助用戶避免安裝來歷不明的盜版APK。
- AndroidManifest.xml:Android清單文件。
2. 常見APK瘦身方案
-
優化assets
- 資源動態下載,字體、js代碼這樣的資源能動態下載的就做動態下載,雖然復雜度提高,但是實現了動態更新
- 壓縮資源文件,用到的時候再進行解壓
- 刪除不必要的字體文件中的字體
- 減少圖標字體(Icon-Font)的使用,多用SVG代替
-
優化lib
-
配置abiFilters精簡so動態庫,而已根據需求保留需要的平臺
defaultConfig {//armeabi是必須包含的,v7是一個圖形加強版本,x86是英特爾平臺的支持庫ndk {abiFilters "armeabi", "armeabi-v7a" ,"x86"} } -
統計分析用戶手機的cpu類型,排除沒有或少量用戶才會用到的so
-
-
優化resources.arsc
- 刪除不必要的string entry,你可以借助android-arscblamer來檢查出可以優化的部分,比如一些空的引用
- 使用微信的資源混淆工具AndResGuard,它將資源的名稱進行了混淆(需要重點配置白名單)
-
優化META-INF:除了公鑰CERT.RSA沒有壓縮機會外,其余的兩個文件都可以通過混淆資源名稱的方式進行壓縮
-
優化res
-
動態下載資源
-
通過Android Studio的重構工具刪除無用資源
-
打包時剔除無用資源
release {zipAlignEnabled trueminifyEnabled true shrinkResources true // 是否去除無效的資源文件 proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'signingConfig signingConfigs.release} -
刪除無用的語言(排除了所有的依賴庫的資源)
android {//...defaultConfig {resConfigs "zh"} } -
控制圖片、視頻、音頻資源的大小,推薦使用有損壓縮等其他格式(ogg、svg、webp等)
-
統一應用風格,減少shape文件
-
使用toolbar,減少menu文件
-
通過復用等方式減少layout文件
-
…
-
-
優化dex:
- 利用工具(dexcount、statistic、apk-method-count、Lint)分析、精簡不必要的方法、空行、依賴庫等
- 通過proguard來刪除無用代碼
- 剔除無用的測試代碼
- 依賴第三方庫的時候,在打包發版中不要將某些不必要的庫進行releaseCompile(比如LeakCanary)
- 使用更小庫或合并現有庫
- 減少方法數、使用插件化等方案,不用mulitdex
3. 參考文章
App瘦身最佳實踐
Android中5種app瘦身方式
進程及進程保活、跨進程通信
1. 查看進程信息
使用ps命令可以查看進程信息:
adb shell ps|grep <package_name>返回的結果分別為:
- 進程當前用戶
- 進程ID
- 進程的父進程ID
- 進程的虛擬內存大小
- 實際駐留”在內存中”的內存大小
- 進程名
2. Android進程優先級
-
前臺進程:Foreground process(用戶正在使用的程序,一般系統是不會殺死前臺進程的,除非用戶強制停止應用或者系統內存不足等極端情況會殺死。)
- 用戶正在交互的Activity(已調用onResume)
- 當某個Service綁定正在交互的Activity
- 被主動調用為前臺Service(startForeground())
- 組件正在執行生命周期的回調(onCreate()、onStart()、onDestory())
- BroadcastReceiver正在執行onReceive()
-
可見進程:Visible process(用戶正在使用,看得到,但是摸不著,沒有覆蓋到整個屏幕,只有屏幕的一部分可見進程不包含任何前臺組件,一般系統也是不會殺死可見進程的,除非要在資源吃緊的情況下,要保持某個或多個前臺進程存活)
- Activity不在前臺、但仍對用戶可見(已調用onPause(),沒有調用onStop()))
- 綁定到可見(前臺)Activity的Service
-
服務進程:Service process(在內存不足以維持所有前臺進程和可見進程同時運行的情況下,服務進程會被殺死)
- 簡單的startService()啟動,與用戶看見的Activity沒有直接關聯。
-
后臺進程:Background process(系統可能隨時終止它們,回收內存)
- 在用戶按了"back"或者"home"后,程序本身看不到了,但是其實還在運行的程序。對用戶沒有直接影響,Activity處于onStop()的時候。
- 應用開啟的進程:android:process=":xxx"
-
空進程:Empty process(某個進程不包含任何活躍的組件時該進程就會被置為空進程,完全沒用,優先級最低,殺了它只有好處沒壞處,第一被回收)
- 不含有任何的活動的組件。(Android設計的,處于緩存的目的,為了第二次啟動更快,采取的一個權衡)
3. 進程回收策略(Low memory Killer)
Low memory Killer:定時執行,一旦發現內存低于某個內存閾值,Low memory Killer會根據進程的優先級、進程占用的內存大小等因素通過復雜的評分機制,對進程進行打分,然后將分數高的進程判定為bad進程,殺死并釋放內存
4. 進程保活方案
- 與手機廠商合作,加入白名單
- 監聽鎖屏廣播,在RemoteService中開啟/關閉一個像素的Activity
- 利用Android5.0以下系統同一時間只能殺死一個進程的漏洞(5.0之后同一個Group的進程都會被殺死),開啟雙進程守護
- 利用前臺服務,startForeground(ID, new Notification()),發送空的通知
- 利用系統黏性服務機制拉活
- 利用開機,網絡切換、拍照、拍視頻等系統廣播也能喚醒,不過Android N已經將這三種廣播取消了
- 利用Native進程監聽進程是否存活,否則拉活
- 利用JobScheduler機制代替Native進程實現拉活
- 利用賬號同步機制拉活。用戶強制停止都殺不起創建一個賬號并設置同步器,創建周期同步,系統會自動調用同步器,這樣就能激活APP,局限是國產機會修改最短同步周期,并且需要聯網才能使用。
5. 跨進程通信方式
6. 參考文章
Android進程保活的一般套路
關于進程保活的兩三事——新手升級經驗卡
Android里帳戶同步的實現
Bitmap
1. Bitmap的理解
Bitmap是Android系統中的圖像處理的最重要類之一。用它可以獲取圖像文件信息,進行圖像剪切、旋轉、縮放等操作,并可以指定格式保存圖像文件。
2. Bitmap的內存分配策略
在Androin3.0之前的版本,Bitmap像素數據存放在Native內存中,而且Nativie內存的釋放是不確定的,容易內存溢出而Crash,不使用的圖片要調用recycle()進行回收。
從Androin3.0開始,Bitmap像素數據和Bitmap對象一起存放在虛擬機的堆內存中(從源代碼上看是多了一個byte[] buffer用來存放數據),也就是我們常說的Java Heap內存。
從Androin8.0開始,Bitmap像素數據存重新回到Native內存中
3. Bitmap的內存占用計算
Bitmap的內存占用大小的計算:
-
一般情況下的計算
Bitmap占用的內存 = width * height * 一個像素所占的內存
-
在Android中,考慮各種因素情況下的計算
Bitmap占用的內存 = width * height * nTargetDensity/inDensity * nTargetDensity/inDensity * 一個像素所占的內存
Bitmap的內存占用大小與三個因素有關:
- 色彩格式,前面我們已經提到,如果是ARGB_8888那么就是一個像素4個字節,如果是RGB_565那就是2個字節
- 原始文件存放的資源目錄(分辨率越小,內存占用越小)
- 目標屏幕的密度(屏幕的密度越小,內存占用越小)
有關Bitmap的色彩格式:
- ARGB_8888的內存消耗是RGB_565的2倍
- ARGB_8888格式比RGB_565多了一個透明通道
- 如果使用RGB_565格式解析ARGB_8888格式的圖片(png),可能會導致圖片變綠
4. Bitmap的回收
- 在Android3.0以前以及Android8.0之后Bitmap的像素數據是存放Native內存中,我們需要回收Native層和Java層的內存。
- 在Android3.0以后以及Android8.0之前Bitmap的像素數據是存放在Java層的內存中的,我們只要回收堆內存即可。
- 官方建議我們3.0以后使用recycle方法進行回收,該方法也可以不主動調用,因為垃圾回收器會自動收集不可用的Bitmap對象進行回收。
- recycle方法會判斷Bitmap在不可用的情況下,將發送指令到垃圾回收器,讓其回收native層和Java層的內存,則Bitmap進入dead狀態。
- recycle方法是不可逆的,如果再次調用getPixels()等方法,則獲取不到想要的結果。
5. Bitmap的復用
Android在3.0之后BitmapFactory.Options引入了inBitmap屬性,設置該屬性之后解碼圖片時會嘗試復用一張已經存在的Bitmap,避免了內存的回收以及重新申請的過程。
Bitmap復用的限制:
- 聲明可被復用的Bitmap必須設置inMutable為true
- Android4.4(API 19)之前只有格式為jpg、png,同等寬高(要求苛刻),inSampleSize為1的Bitmap才可以復用
- Android4.4(API 19)之前被復用的Bitmap的inPreferredConfig會覆蓋待分配內存的Bitmap設置的inPreferredConfig
- Android4.4(API 19)之前待加載Bitmap的Options.inSampleSize必須明確指定為1
- Android4.4(API 19)之后被復用的Bitmap的內存必須大于需要申請內存的Bitmap的內存
6. Bitmap加載大圖與防止OOM
加載大圖的時候注意點:
- 在Android系統中,讀取位圖Bitmap時,分給虛擬機中的圖片的堆棧大小只有8M,如果超出了,就會出現OutOfMemory異常
- 在加載大圖、長圖等操作當中,推薦對OutOfMemoryError進行捕獲,并且返回一張默認圖片
- 使用采樣率(inSampleSize),如果需要顯示縮列圖,并不需要加載完整的圖片數據,只需要按一定的比例加載即可
- 使用Matrix變形等,比如使用Matrix進行放大,雖然圖像大了,但并沒有占用更多的內存
- 推薦使用一些成熟的開源圖片加載庫,它們幫我們完成了很多工作。比如異步加載、Facebook的Fresco還自己開辟了Native內存用于存儲圖片,以得到更大的內存空間(兼容性問題)
- 使用分塊解碼(BitmapRegionDecoder)、硬解碼等方案
獲取圖片縮略圖的模板代碼如下(主要分為3個步驟):
public static Bitmap thumbnail(String path, int width, int height, boolean autoRotate) {//1. 獲得Bitmap的寬高,但是不加載到內存BitmapFactory.Options options = new BitmapFactory.Options();options.inJustDecodeBounds = true;BitmapFactory.decodeFile(path, options);int srcWidth = options.outWidth;int srcHeight = options.outHeight;//2. 計算圖片縮放倍數int inSampleSize = 1;if (srcHeight > height || srcWidth > width) {if (srcWidth > srcHeight) {inSampleSize = Math.round(srcHeight / height);} else {inSampleSize = Math.round(srcWidth / width);}}//3. 真正加載圖片到內存當中options.inJustDecodeBounds = false;options.inSampleSize = inSampleSize;//ARGB_8888格式的圖片,每像素占用 4 Byte,而 RGB565則是 2 Byteoptions.inPreferredConfig = Bitmap.Config.RGB_565;options.inPurgeable = true;options.inInputShareable = true;return BitmapFactory.decodeFile(path, options); }7. LRU緩存機制
LRU緩存機制的核心原理:
- LruCache中維護了一個以訪問順序排序的集合LinkedHashMap(雙向循環鏈表)
- 新數據插入到鏈表頭部
- 每當緩存命中(即緩存數據被訪問),則將數據移到鏈表頭部
- 當鏈表滿的時候,將鏈表尾部的數據丟棄
8. 圖片的三級緩存
關于Android圖片的多級緩存,其中主要的就是內存緩存和硬盤緩存。它的核心思想是:
- 在獲取一張圖片時,首先到內存緩存(LruCache)中去加載
- 如果未加載到,則到硬盤緩存(DiskLruCache)中加載,如果加載到將其返回并添加進內存緩存
- 否則通過網絡加載一張新的圖片,并將新加載的圖片添加進入內存緩存和硬盤緩存
9. 參考文章
Bitmap詳解與Bitmap的內存優化
Android性能調優(5)—Bitmap內存模型
Android面試一天一題(Day 22: 圖片到底是什么)
Android使用BitmapRegionDecoder加載超大圖片方案
Android性能調優(6)—Bitmap優化
徹底解析Android緩存機制——LruCache
淺談圖片加載的三級緩存
數據存儲方式與數據緩存
1. Android中5大數據存儲方式
- 文件:存儲一些簡單的文本數據或者二進制數據,包括內存、外部存儲。
- SharedPreferences(本質上屬于文件存儲):適用于存儲一些鍵值對,一般用來存儲配置信息。
- SQLite數據庫:適用于存儲一些復雜的關系型數據。
- 網絡:存儲比較重要的數據,比如賬號密碼等。
- ContentProvider:訪問(增刪改查)其他應用程序中私有數據。
2. 有關數據安全問題
- 避免使用靜態變量保存核心、重要的數據。因為靜態變量在進程被殺死的時候,會重新初始化
- 官方推薦使用五大數據存儲方式
3. 數據緩存
數據緩存
- 推薦使用LRU策略緩存
- 內存緩存:存儲在集合當中,重要數據不推薦使用靜態變量存儲
- 磁盤緩存:根據數據類型、應用場合選擇Android中5大數據存儲方式實現緩存
- 使用一些實現了內存緩存、磁盤緩存、緩存時間等強大功能的第三方開源的緩存庫:ACache、RxCache等
緩存統計顯示與緩存清理
- 緩存統計與顯示:統計/data/你的應用包名/cache/目錄的大小
- 緩存清理:刪除/data/你的應用包名/cache/目錄
4. 參考文章
安卓中五種數據存儲方式
【Android開源項目分析】android輕量級開源緩存框架——ASimpleCache(ACache)源碼分析
【從 0 開始開發一款直播 APP】6 緩存 ACache 源碼解析
Android記錄20-獲取緩存大小和清除緩存功能
SharedPreferences
1. SharedPreferences基本概念以及優缺點
- SharedPreferences用來保存基于XML文件存儲的key-value鍵值對數據,通常用來存儲一些簡單的配置信息。數據存儲在/data/data//shared_prefs目錄下。
- 只能存儲少量boolean、int、float、long、String五種簡單的數據類型,操作簡單,但是無法完全替代其他數據存儲方式。
- 無法進行條件查詢等復雜操作,對于數據的處理只能是簡單的處理。
2. SharedPreferences的實現原理
SharedPreferences是個單例,具體實現在SharedPrefencesImpl。任意Context拿到的都是同一個實例。
SharedPreferences在實例化的時候會把SharedPreferences對應的xml文件內容通過pull解析全部讀取到內存當中(mMap)。
關于讀操作:對于非多進程兼容的SharedPreferences的讀操作是從內存讀取的,不涉及IO操作。寫入的時候由于內存已經保存了完整的xml數據,然后新寫入的數據也會同步更新到內存,所以無論是用commit還是apply都不會影響立即讀取。
關于寫操作:除非需要關心xml是否寫入文件成功,否則你應該在所有調用commit的地方改用apply。
3. 同步與異步提交
commit方法的特點
- 存儲的過程是原子操作
- commit方法有返回值,設置成功為ture,否則為false
- 同時對一個SharedPreferences設置值最后一次的設置會直接覆蓋前次值
- 如果不關心設置成功與否,并且是在主線程設置值,建議用apply方法
apply方法的特點
- 存儲的過程也是原子操作
- apply沒有返回值,存儲是否成功無從知道
- apply寫入過程分兩步,第一步先同步寫入內存,第二部在異步寫入物理磁盤
- apply寫入的過程會阻塞同一個SharedPreferences對象的其他寫入操作
總結
apply比commit效率高,commit直接是向物理介質寫入內容,而apply是先同步將內容提交到內存,然后在異步的向物理介質寫入內容。這樣做顯然提高了效率。
4. SharedPreferences不能實現跨進程同步問題
現象
- 數據安全問題:數據讀寫不能實時更新而造成數據寫入丟失等問題,每個進程都會維護一個SharedPreferences的內存副本,副本之間互不干擾
- getSharedPreferences時候的空指針問題
解決方案
- 通過查看 API 文檔發現,在API Level > 11即Android 3.0可以通過Context.MODE_MULTI_PROCESS屬性來實現多進程間的數據共享
- 但是在API 23時該屬性被廢棄。官方文檔中明確寫明SharedPreferences不適用于多進程間共享數據,推薦使用ContentProvider等方式
5. SharedPreferences存儲的數據不能過大
- SharedPreferences存儲的基本的配置型數據,不能存儲大量數據
- SharedPreferences的讀寫操作可能會阻塞主線程,引起界面卡頓甚至ANR
- SharedPreferences的Key-Value的mMap是一直存放在內存當中的,這樣會帶來極大的內存消耗,甚至產生泄漏、OOM
- SharedPreferences對Key-Value頻繁讀寫會產生大量的臨時對象,會造成內存抖動,頻繁GC會造成界面卡頓等問題
6. 參考文章
Android面試一天一題(14 Day:SharedPreferences)
SharedPreferences多進程共享數據爬坑之旅
深入理解Android SharedPreferences的commit與apply
SharedPreferences commit跟apply的區別
組件化
1. 組件化的基本概念
組件化開發就是將一個app分成多個模塊,每個模塊都是一個組件(Module),開發的過程中我們可以讓這些組件相互依賴或者單獨調試部分組件等,但是最終發布的時候是將這些組件合并統一成一個apk,這就是組件化開發。
2. 組件化的特點
- 公共資源、業務、模塊混在一起耦合度太高,組件化可以實現模塊之間的解耦、單獨測試驗證,又實現了模塊共享資源和工具類
- 組件化可以提高開發效率:每個模塊可以獨立開發編譯運行
- 利用組件化的思想對開源框架進行一次封裝,除了防止代碼入侵以外,同時也簡化了使用,實現了項目的需求
3. 組件化的實現
組件化只是一種項目架構的思想,并沒有具體的實現方案,需要根據公司的業務、項目性質等進行具體實現。一般的套路如下:
組件化中,最好提供一個統一路由方案實現模塊之間的分發和跳轉。
4. 參考文章
Android組件化和插件化開發
Android組件化方案
插件化
1. 插件化的基本概念
插件化開發和組件化開發略有不用,插件化開發時將整個app拆分成很多模塊,這些模塊包括一個宿主和多個插件,與組件化最大的不同是:插件化中每個模塊都是一個單獨的apk(組件化的每個模塊是個lib),最終打包的時候將宿主apk和插件apk分開或者聯合打包。
2. 插件化的特點
- 解決應用越來越大所帶來的技術限制,比如65535方法數量限制
- 應用越來越大的時候多人合作開發的問題:插件化可以實現宿主和插件分開編譯、并行開發,提高開發效率并且分工明確
- 插件模塊的動態按需下載,減少宿主APK的體積
- 插件模塊的動態更新可以解決線上BUG或者上架活動,達到熱更新、熱修復的目的
- 帶有插件化、熱更新的APK不能在Google Play上線,也就是沒有海外市場
3. 插件化的核心原理
- 通過類加載機制(DexClassLoader)加載插件APK
- 通過代理機制(主要是動態代理)實現Activity等組件的生命周期
- 通過Java的反射機制結合面向接口(抽象)編程、面向切面程,實例化并且調用插件中的代碼
- 訪問插件中的資源:通過反射生成AssetManager對象并且通過反射調用addAssetPath方法加載插件中的資源(資源、主題等)
- 利用Hook機制對ActivityManagerService進行Hook,實現啟動一個沒有在清單文件中注冊的插件Activity
- 不同框架的具體實現和原理都不一樣……
4. 常見插件化框架(按照時間先后排列)
AndroidDynamicLoader:Android 動態加載框架,他不是用代理 Activity 的方式實現而是用 Fragment 以及 Schema 的方式實現
PluginMgr:不需要插件規范的apk動態加載框架。
Dynamic-load-apk:Android 使用動態加載框架DL進行插件化開發
Direct-Load-apk:Direct - load - apk 能夠加載插件的全部 資源. 支持 插件間 Activity跳轉. 不像 “dynamic load - apk” 這個項目, “Direct - load - apk” 不需要對插件有任何約束,也不需要在插件中引入jar和繼承自定義Activity,可以直接使用this指針。
Android-Plugin-Framework:此項目是Android插件開發框架完整源碼及示例。用來通過動態加載的方式在宿主程序中運行插件APK
ACDD:非代理Android動態部署框架
DynamicAPK:實現Android App多apk插件化和動態加載,支持資源分包和熱修復.攜程App的插件化和動態加載框架
比較新的,有代表性的有下面4個:
DroidPlugin:是360手機助手在Android系統上實現了一種新的插件機制
Small:世界那么大,組件那么小。Small,做最輕巧的跨平臺插件化框架
VirtualAPK:VirtualAPK是滴滴出行自研的一款優秀的插件化框架
RePlugin:RePlugin是一套完整的、穩定的、適合全面使用的,占坑類插件化方案,由360手機衛士的RePlugin Team研發,也是業內首個提出”全面插件化“(全面特性、全面兼容、全面使用)的方案
5. 參考文章
Android插件化技術入門
插件化開發小結
Android博客周刊專題之 插件化開發
Android開源插件化框架匯總
熱修復
1. 熱修復主要解決的問題
- 版本發布之后發現嚴重BUG,需要緊急動態修復
- 小功能即時上線、下線,比如節日活動
2. 傳統開發流程與熱修復開發流程對比
從流程來看,傳統的開發流程存在很多弊端:
- 重新發布版本代價太大
- 用戶下載安裝成本太高
- BUG修復不及時,用戶體驗太差
而熱修復的開發流程顯得更加靈活,優勢很多:
- 無需重新發版,實時高效熱修復
- 用戶無感知修復(甚至無需重啟應用),無需下載新的應用,代價小
- 修復成功率高,把損失降到最低
3. 熱修復補丁修復詳細工作流程
4. 熱修復的兩大核心原理
- ClassLoader加載方案
- Native層替換方案(Hook Native)
5. 熱修復主流框架及實現原理
QQ空間的超級補丁技術
超級補丁技術基于DEX分包方案,使用了多DEX加載的原理,大致的過程就是:把BUG方法修復以后,放到一個單獨的DEX里,插入到dexElements數組的最前面,讓虛擬機去優先加載修復完后的方法。
當patch.dex中包含Test.class時就會優先加載,在后續的DEX中遇到Test.class的話就會直接返回而不去加載,這樣就達到了修復的目的。
微信的Tinker
微信針對QQ空間超級補丁技術的不足提出了一個提供DEX差量包,整體替換DEX的方案。主要的原理是與QQ空間超級補丁技術基本相同,區別在于不再將patch.dex增加到elements數組中,而是差量的方式給出patch.dex,然后將patch.dex與應用的classes.dex合并,然后整體替換掉舊的DEX文件,以達到修復的目的。
阿里的AndFix
AndFix不同于QQ空間超級補丁技術和微信Tinker通過增加或替換整個DEX的方案,提供了一種運行時通過Hook Native方法,在Native修改Filed指針的方式,實現Java方法的替換,達到即時生效無需重啟,對應用無性能消耗的目的。
美團的Robust
主要原理:在每個方法前加入一段代碼,如果patch.jar存在,則加載patch.jar中的代碼片段,否則執行原本的代碼片段。
餓了么的Amigo
Amigo 原理與 Tinker 基本相同,但是在 Tinker 的基礎上,進一步實現了 so 文件、資源文件、Activity、BroadcastReceiver 的修復,幾乎可以號稱全面修復,不愧 Amigo(朋友)這個稱號,能在危急時刻送來全面的幫助。
6. 三大主流框架對比
7. 參考文章
Android熱修復技術選型——三大流派解析
Android 插件化和熱修復知識梳理
路由
1. 什么是路由?
根據路由表將頁面請求分發到指定頁面。
2. 為什么需要路由?(路由的應用場景)
- 解耦:APP中使用到組件化、插件化等技術的時候,需要解耦模塊、頁面之間的依賴關系
- 跳轉一致化:原生、H5頁面之間的互相跳轉規范統一
- 頁面動態配置:APP收到消息推送,點擊通知/用戶點擊活動Banner廣告等需要跳轉到某個頁面,該頁面可以動態配置
- 頁面跳轉的攔截:比如不合法的頁面的屏蔽、登錄驗證攔截、頁面不存在需要攔截并且跳轉到下載頁面等
3. 路由的優勢(與不使用路由對比)
- 路由實現了頁面之間解耦:顯式Intent在項目龐大以后,類依賴耦合太大,不適合組件化拆分
- 路由簡化了頁面之間的協作:隱式Intent協作困難,調用時候不知道調什么參數
- 路由的攔截功能提高了頁面訪問的安全性:每個注冊了Scheme的Activity都可以直接打開,有安全風險
- 路由表的動態生成簡化了頁面的管理:解決了傳統的AndroidMainfest集中式管理,這樣比較臃腫的問題
- 路由的動態降級功能:不使用路由,如果頁面出錯,無法動態降級
- 路由的動態攔截功能:不使用路由,每次打開新的頁面都需要做登錄驗證
- 路由實現了頁面統一跳轉:H5、Android、iOS地址不一樣,不利于統一相互跳轉協議、不利于服務器端(例如推送、活動Banner等)對APP頁面進行動態配置
4. 路由的常見功能及其實現思路和核心原理
-
路由注冊
- AndroidManifest里面的Activity聲明scheme碼是不安全的,所有App都可以打開這個頁面,這里就產生有三種方式去注冊:
- 注解產生路由表,通過DispatchActivity轉發Intent
- AndroidManifest注冊,將其export=false,再通過DispatchActivity轉發Intent,天貓就是這么做的,比上面的方法的好處是路由查找都是系統調用,省掉了維護路由表的過程,但是AndroidManifest配置還是比較不方便的
- 注解自動修改AndroidManifest,這種方式可以避免路由表匯總的問題,方案是這樣的,用自定義Lint掃描出注解相關的Activity,然后在processManifestTask后面修改AndroidManifest,該方案不穩定
-
路由表生成
- 用APT(Annotation Processing Tool)生成URL和Activity的對應關系、并且(結合路由分組策略)進行路由匯總
-
路由分發
- 現在所有路由方案分發都是用Activity做分發的
-
結果返回
- 捕獲onActivityResult
-
動態攔截
- 攔截器是重中之重,有了攔截器可以做好多事情。ARouter是用線程等待實現的(等所有注冊的攔截器處理完成之后才會進行下一步分發跳轉)
-
參數獲取
- 大部分路由庫都是手動獲取參數的,這樣還要傳入參數key比較麻煩,有三種做法:
- Hook掉Instrumentation的newActivity方法,注入參數
- 注冊ActivityLifecycleCallbacks方法,注入參數
- APT生成注入代碼,onCreate的時候bind一下
5. 常見的路由框架對比
6. 參考文章
需要給activity跳轉增加路由么?
Android 組件化 —— 路由設計最佳實踐
Android 路由框架ARouter最佳實踐
開源最佳實踐:Android平臺頁面路由框架ARouter
一款可能是最容易使用的對頁面、服務的路由框架。使用APT實現
RouterKit
Android權限機制和動態權限適配
1. 什么是Android權限機制
- Android6.0以前:開發者在AndroidManifest文件中聲明需要的權限,APP安裝時,系統提示用戶APP將獲取的權限,需要用戶同意授權才能繼續安裝,從此APP便永久的獲得了授權。
- Android6.0之后:引入動態權限的機制。在APP運行時,用戶可以根據自身的需要,決定是否授予APP危險權限,同時,用戶也可以很方便回收授予的權限。動態權限管理的機制,對于用戶的隱私保護是更加適用的。
2. 危險權限(組)與一般權限
- 危險權限(組)Dangerous Permissions:聯系人、電話、相機、定位、存儲、錄音、短信、日歷、傳感器
- 一般權限Normal Permissions:除危險權限之外的權限,比如網絡、WIFI、藍牙、鬧鐘、壁紙、NFC等
注意:
- 對于危險權限的分組,如果申請某個危險的權限,假設app早已被用戶授權了同一組的某個危險權限,那么系統會立即授權,而不需要用戶去點擊授權。
- 不要對權限組過多的依賴,盡可能對每個危險權限都進行正常流程的申請,因為在后期的Android版本中這個權限組可能會產生變化。
3. 如何適配Android6.0動態權限及其兼容性問題
動態權限適配
-
APP要適配Android6.0非常簡單,只需要在清單文件中配聲明權限,然后將targetSdkVersion升級到23及以上,同時加入權限檢查、申請、處理申請結果等代碼邏輯即可。
-
相關重要API:
- 檢查權限:ContextCompat.checkSelfPermission()
- 申請授權:ActivityCompat.requestPermissions()
- 處理回調:onRequestPermissionsResult()
-
權限適配的代碼封裝,同時也可以使用一些封裝好的開源庫:MPermission、RxPermission等
動態權限的運行兼容性問題
- 首先,舊版本APP(targetSdkVersion低于23),因為沒有適配權限的申請相關邏輯,在Android6.0以上機型運行的時候,仍然采用安裝時授權的方案。
- 適配了Android6.0的APP,在低版本Android系統上運行的時候,仍然采用安裝時授權的方案。
- 開發者需要注意的是,權限申請的代碼邏輯只應該在Android6.0及以上的機型被執行。(因此推薦使用XXXCompat的類,這種類已經對Android版本進行了判斷)
4. 參考文章
Android 權限機制與適配經驗(QQ音樂)
Android 6.0 運行時權限處理完全解析(鴻洋)
View的繪制以及事件傳遞機制
http://hencoder.com/
動畫機制
屏幕適配
Android新特性介紹,ConstraintLayout完全解析
設計模式與架構
Kotlin
開源框架源碼分析
Java高級基礎
大綱
Android工程師之Android面試大綱
面試復習——Android工程師之Android面試大綱
歡迎進入Hensen_的博客目錄(全站式導航)
總結
以上是生活随笔為你收集整理的Android面试复习资料整理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第13节 IIS之WEB服务器—用于发布
- 下一篇: Android热修复技术总结