本文微信公衆號「AndroidTraveler」首發。java
最近部門有新入職員工,做爲規劃技術路線的導師,這邊給新員工安排了學習路線。android
除了基本的學習路線以外,每次溝通,我都留了一個小問題,讓小夥伴去思考。數據庫
這些問題有些是剛接觸 Android 開發的小夥伴所不熟悉的,有些則是部分初級工程師都沒有注意到的。微信
所以這邊紀錄一下,但願幫助剛畢業進入職場的 Android 小夥伴,或是對這些還不是很熟悉的 Android 開發工程師們。app
若有補充或者交流,歡迎留言。異步
Q: 你是否瞭解過 ANR?
A: 知道,但不是很瞭解。async
Q: 什麼狀況下會出現 ANR?
假設這裏回答的不是「在主線程執行耗時任務」的話,能夠不繼續追問,直接讓小夥伴去了解 ANR,後期再討論。
若是回答了是「在主線程執行耗時任務」的話,那麼繼續:ide
Q: 多久算耗時?
A: 不要超過 5s。函數
Q: 那麼假設我在 Activity sleep 20s,是否是就必定會 ANR?學習
上代碼例子:
MainActivity.java 文件:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Log.e("zengyu","before sleep"); try { Thread.sleep(20000); } catch (InterruptedException e) { e.printStackTrace(); } Log.e("zengyu","after sleep"); } }); } }
activity_main.xml 文件:
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/button" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout>
代碼功能很簡單,就是一個按鈕,點擊以後會 sleep 20 秒。在 sleep 前和 sleep 後都會打印日誌。
若是你只是點擊按鈕,而後什麼都不動,是不會有 ANR 的。
可是你點擊了按鈕以後,你繼續屢次點擊按鈕,那麼就會有 ANR 了。
如下四個條件均可以形成ANR發生:
InputDispatching Timeout: 5秒內沒法響應屏幕觸摸事件或鍵盤輸入事件
BroadcastQueue Timeout : 在執行前臺廣播(BroadcastReceiver)的onReceive()函數時10秒沒有處理完成,後臺爲60秒。
Service Timeout : 前臺服務20秒內,後臺服務在200秒內沒有執行完畢。
ContentProvider Timeout : ContentProvider的publish在10s內沒進行完。
因此可能不少小夥伴會把上面四個條件的第一個和 Activity 直接掛鉤,覺得是在主線程耗時超過 5s 就會 ANR。其實是 InputDispatching。
Q: 既然主線程不能作耗時任務,那麼有耗時任務怎麼辦?
A: 經過 new Thread 啓動一個子線程,在子線程處理。
Q: 考慮一個場景,好比相似微信這類 IM 軟件收到消息。須要寫數據庫,這個時候須要啓動線程。當收到消息 N 多的時候,若是都用 new Thread 啓動線程的話,是否會有問題。場景模擬能夠經過循環建立子線程模擬。
上代碼例子:
MainActivity.java 文件:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); for (int i = 0; i < 10000; i++) { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } } }
這個部分手機廠商好比(華爲)有對線程數目作限制的話,一運行就會 crash,Logcat 會看到下面信息:
pthread_create (1040KB stack) failed: Out of memory
我這邊一開始在三星 S7 上面運行,並無出現。後面換成華爲 5x 手機就出現了。
Android 開發的小夥伴都知道兼容是硬傷,因此咱們不能抱有僥倖心理。
針對這種狀況,咱們不能一遇到耗時任務,就很瀟灑的一個 new Thread 所有搞定。
若是你當前界面只有一個耗時任務,並且只須要調用一次,那麼你進入該界面用 new Thread 來處理沒有問題。
可是假設像上面咱們描述的場景那樣,須要調用屢次的時候。你就不能簡單粗暴的使用 new Thread 了。
推薦方式是使用線程池。
一個緣由是避免一些廠商的線程數目限制。
另外一個緣由是減小線程的頻繁建立和銷燬。
Q: 上面咱們說到了,若是界面調用一次,並且須要啓動線程的時候。可使用 new Thread 建立,那麼直接使用 new Thread 可能還有什麼問題嗎?
這裏想考察的點可能比較晦澀一點。
因爲內部類會持有外部類的引用。假設在 Activity 裏面經過匿名內部類的方式來啓動線程作耗時任務。當用戶退出界面時,因爲內部類還持有 Activity 的引用,所以 Activity 無法獲得釋放。
就會存在內存泄漏問題。
解決方法也比較統一,那就是將內部類改成靜態內部類。
因此修改後的代碼對好比下:
修改前:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); new Thread(new Runnable() { @Override public void run() { //TODO } }).start(); } }
修改後:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); new StaticThread().start(); } private static class StaticThread extends Thread { @Override public void run() { super.run(); //TODO } } }
Q: 用過 Handler 嗎?
A: 用過。
Q: 寫一下簡單的 demo 我看下。
這個是緊接第三點。若是不涉及界面交互,只須要使用到靜態內部類就能夠解決。可是當 Handler 裏面須要作界面更新處理時,那麼須要使用弱引用。由於靜態內部類的處理方式原本就是爲了不 Activity 沒法獲得釋放。你若是把 Activity 直接傳進來,那麼 Activity 的引用被靜態內部類持用了,因此這個時候就須要使用到弱引用了。
直接上代碼:
public class MainActivity extends AppCompatActivity { private static class StaticHandler extends Handler { private WeakReference<MainActivity> activityWeakReference; public StaticHandler(MainActivity mainActivity) { this.activityWeakReference = new WeakReference<>(mainActivity); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); //TODO //use activityWeakReference.get() to get view } } private StaticHandler mStaticHandler = new StaticHandler(this); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mStaticHandler.sendEmptyMessage(0); } }
通常不少網上教程和例子在 SharedPreferences 的數據寫入時,通常都會使用 Editor 的 commit 方法。
因爲 commit 方法是同步寫入的。而且寫文件屬於 I/O 操做,若是你有大量的數據須要寫入,而且你是在主線程處理的,可能會致使流暢性受影響。極端狀況下可能會出現 ANR。
咱們點進去源碼看下:
If you don't care about the return value and you're using this from your application's main thread, consider using {@link #apply} instead.
其實源碼也說的很清楚了。若是你不關心返回值而且你是在應用的主線程使用的話,考慮使用 apply 替換 commit。
因爲咱們通常不會處理返回值,所以建議使用 apply 替換 commit。
apply 會把變化立刻寫進內存,而後經過異步方式去寫入。
Unlike {@link #commit}, which writes its preferences out to persistent storage synchronously, {@link #apply} commits its changes to the in-memory {@link SharedPreferences} immediately but starts an asynchronous commit to disk and you won't be notified of any failures.
固然源碼還有一個註釋以下:
If another editor on this {@link SharedPreferences} does a regular {@link #commit} while a {@link #apply} is still outstanding, the {@link #commit} will block until all async commits are completed as well as the commit itself.
大概意思就是 apply 若是在處理中還未完成的狀況下,commit 會阻塞直到全部異步操做完成纔會去 commit。
所以若是要替換,建議將 commit 都替換爲 apply。
這裏主要是考慮相似微信 IM 登陸後拉取大量離線消息寫入數據庫的問題。
經過對比開啓事務和不開啓事務的耗時來進行說明。
好比不開啓事務插入 10000 條紀錄和開啓事務插入 10000 條紀錄耗時對比。
對於大量的數據庫操做,建議開啓事務的方式,速度的提高是很明顯的。
參考連接:
Android ANR:原理分析及解決辦法