Android 學習筆記思考篇

薛定貓的諤發表於2019-06-28

概述

Android 系統從 2008 年正式釋出到現在已經過去了 10 年,系統版本也來到了 9,作為開發者,或者作為使用者,我們見證了系統一次次大大小小的改動,見證了系統的不斷完善,見證了我們寫的每個 Android 小程式給我們帶來的成就感。但是,當我們寫的程式越來越多時,當我們對 Android 應用開發越來越瞭解時,我們發現它並不完美,甚至有些簡陋:
Service 從字面上理解就是後臺服務,一個看不見的服務不應該執行在後臺嗎?不應該執行在獨立的程式中嗎?就算執行在主程式中那不應該執行在後臺執行緒中嗎?
文件中確實提醒過不要在主執行緒中進行耗時操作,那為什麼在主執行緒中讀寫檔案沒有問題?甚至連警告都沒有?讀寫 SharedPreferences 檔案算不算讀寫檔案?算不算耗時操作?
把耗時操作放在後臺執行緒中執行,那意味著我們需要精通 JUC?需要建立執行緒,維護執行緒,把執行緒變成什麼 Looper 執行緒才能用 Handler 通訊,還得考慮執行緒安全,什麼?為了效能和防止無限建立執行緒引發問題還要了解並使用執行緒池技術?用執行緒池就不會有問題了麼?我們能不能不關心執行緒、執行緒池、LooperHandler 什麼的,我們就是想單純地讓這段程式碼非同步執行而已,奧,原來有 AsyncTask 就不用關心這些了啊,那我們還需要維護這些 AsyncTask 嗎?這些非同步任務的生命週期能跟檢視元件繫結嗎?不能的話怎麼手動維護這些 AsyncTask 啊?
非同步任務執行完之後我們想直接顯示個對話方塊行不行?什麼?得先判斷 Activity 的狀態才能顯示?不判斷好像也沒什麼問題啊?退出 Activity 的時候還需要手動關閉各種對話方塊?不關閉好像也沒什麼問題啊?

非同步

Android 中的非同步操作基本都是使用 Java 語言內建的,唯一的簡單封裝的非同步類 AsyncTask 有幾個主要回撥,我們可以通過這些回撥指定那些程式碼在非同步任務開始之前執行,哪些程式碼在非同步任務中執行,哪些程式碼在任務執行完成後執行:

static class Task extends AsyncTask<Integer, Integer, String> {
    String taskDesc;
    public Task(String taskDesc) {
        this.taskDesc = taskDesc;
    }
    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        Log.e(TAG, taskDesc + ": " + "onPreExecute");
    }
    @Override
    protected String doInBackground(Integer... integers) {
        Log.e(TAG, taskDesc + ": " + "doInBackground " + Thread.currentThread());
        String ret = null;
        int[] array = new int[1000000];
        for (int i = 0; i < array.length; i++) {
            array[i] = i;
        }
        for (int i = 0; i < 1000000; i++) {
            long sum = 0;
            for (int j = 0; j < integers[0]; j++) {
                sum += array[j];
            }
            ret = String.valueOf(sum);
            mTotalCount++;
        }
        return ret;
    }
    @Override
    protected void onPostExecute(String s) {
        super.onPostExecute(s);
        Log.e(TAG, taskDesc + ": " + "onPostExecute " + s + ", " + mTotalCount);
    }
}
複製程式碼

我們在非同步任務中執行一個很簡單但很耗時的計算:算一百萬次陣列的區間和,現在我們來執行一下這個非同步任務:

mTask = new Task("task-1").execute(300);
...
@Override
protected void onDestroy() {
    super.onDestroy();
    mTask.cancel(true);
}
複製程式碼
16:24:40.361 E/task: task-1: onPreExecute
16:24:40.365 E/task: task-1: doInBackground Thread[AsyncTask #1,5,main]
16:24:46.778 E/task: task-1: onPostExecute 44850, 1000000
複製程式碼

從輸出日誌中可以看到大約 6 秒後非同步任務執行完了,算出了從 0 加到 300 的結果是 44850(如果還記得等差數列的求和公式那麼你肯定已經知道了 44850 確實是個正確的計算結果),我們用來統計計算次數的變數也是正確的,確實是一百萬次。現在我們同時執行 10 個這樣的任務再看一下:

for (int i = 0; i < 10; i++) {
    mTaskList.add(new Task("task-" + i).execute(300));
}
...
@Override
protected void onDestroy() {
    super.onDestroy();
    for (AsyncTask task : mTaskList) {
        task.cancel(true);
    }
}
複製程式碼
16:42:06.313 E/task: task-0: onPreExecute
16:42:06.316 E/task: task-1: onPreExecute
16:42:06.316 E/task: task-2: onPreExecute
16:42:06.316 E/task: task-3: onPreExecute
16:42:06.316 E/task: task-4: onPreExecute
16:42:06.316 E/task: task-5: onPreExecute
16:42:06.316 E/task: task-6: onPreExecute
16:42:06.316 E/task: task-7: onPreExecute
16:42:06.317 E/task: task-0: doInBackground Thread[AsyncTask #1,5,main]
16:42:06.317 E/task: task-8: onPreExecute
16:42:06.317 E/task: task-9: onPreExecute
16:42:12.724 E/task: task-0: onPostExecute 44850, 1000000
16:42:12.726 E/task: task-1: doInBackground Thread[AsyncTask #2,5,main]
16:42:17.712 E/task: task-1: onPostExecute 44850, 2000000
16:42:17.715 E/task: task-2: doInBackground Thread[AsyncTask #3,5,main]
16:42:22.706 E/task: task-2: onPostExecute 44850, 3000000
16:42:22.708 E/task: task-3: doInBackground Thread[AsyncTask #4,5,main]
16:42:27.710 E/task: task-3: onPostExecute 44850, 4000000
16:42:27.710 E/task: task-4: doInBackground Thread[AsyncTask #4,5,main]
16:42:32.698 E/task: task-4: onPostExecute 44850, 5000000
16:42:32.698 E/task: task-5: doInBackground Thread[AsyncTask #4,5,main]
16:42:37.682 E/task: task-5: onPostExecute 44850, 6000000
16:42:37.683 E/task: task-6: doInBackground Thread[AsyncTask #4,5,main]
16:42:42.672 E/task: task-6: onPostExecute 44850, 7000000
16:42:42.672 E/task: task-7: doInBackground Thread[AsyncTask #4,5,main]
16:42:47.661 E/task: task-7: onPostExecute 44850, 8000000
16:42:47.663 E/task: task-8: doInBackground Thread[AsyncTask #5,5,main]
16:42:52.655 E/task: task-8: onPostExecute 44850, 9000000
16:42:52.657 E/task: task-9: doInBackground Thread[AsyncTask #6,5,main]
16:42:57.644 E/task: task-9: onPostExecute 44850, 10000000
複製程式碼

什麼情況?所有的非同步任務為什麼是一個接一個執行的啊?這個設定真的是太難以接受了
作者在封裝 AsyncTask 這個類時多個任務是在一個後臺執行緒中序列執行的,後來才意識到這樣效率太低了就從 Android 1.6(API Level 4)開始改成並行執行了,但是從 Android 3.0(API Level 11)開始又改成預設序列執行了,Google 給的解釋是為了避免並行執行可能帶來的錯誤???如果你一定要並行執行,需要使用 executeOnExecutor() 方法並使用類似 AsyncTask.THREAD_POOL_EXECUTOR 這樣的執行緒池去執行任務。既然這樣,我們試一下:

for (int i = 0; i < 10; i++) {
    mTaskList.add(new Task("task-" + i).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, 300));
}
...
@Override
protected void onDestroy() {
    super.onDestroy();
    for (AsyncTask task : mTaskList) {
        task.cancel(true);
    }
}
複製程式碼
17:26:26.867 E/task: task-0: onPreExecute
17:26:26.870 E/task: task-1: onPreExecute
17:26:26.870 E/task: task-2: onPreExecute
17:26:26.870 E/task: task-0: doInBackground Thread[AsyncTask #1,5,main]
17:26:26.871 E/task: task-3: onPreExecute
17:26:26.871 E/task: task-1: doInBackground Thread[AsyncTask #2,5,main]
17:26:26.874 E/task: task-4: onPreExecute
17:26:26.874 E/task: task-5: onPreExecute
17:26:26.874 E/task: task-6: onPreExecute
17:26:26.874 E/task: task-3: doInBackground Thread[AsyncTask #4,5,main]
17:26:26.875 E/task: task-7: onPreExecute
17:26:26.875 E/task: task-8: onPreExecute
17:26:26.875 E/task: task-9: onPreExecute
17:26:26.875 E/task: task-2: doInBackground Thread[AsyncTask #3,5,main]
17:26:33.434 E/task: task-4: doInBackground Thread[AsyncTask #2,5,main]
17:26:33.434 E/task: task-5: doInBackground Thread[AsyncTask #4,5,main]
17:26:33.436 E/task: task-1: onPostExecute 44850, 3951253
17:26:33.436 E/task: task-3: onPostExecute 44850, 3951347
17:26:33.485 E/task: task-6: doInBackground Thread[AsyncTask #1,5,main]
17:26:33.486 E/task: task-0: onPostExecute 44850, 3984209
17:26:33.528 E/task: task-7: doInBackground Thread[AsyncTask #3,5,main]
17:26:33.529 E/task: task-2: onPostExecute 44850, 4014638
17:26:38.641 E/task: task-8: doInBackground Thread[AsyncTask #4,5,main]
17:26:38.643 E/task: task-9: doInBackground Thread[AsyncTask #2,5,main]
17:26:38.643 E/task: task-5: onPostExecute 44850, 7900003
17:26:38.644 E/task: task-4: onPostExecute 44850, 7900500
17:26:38.720 E/task: task-7: onPostExecute 44850, 7958289
17:26:38.757 E/task: task-6: onPostExecute 44850, 7974684
17:26:43.671 E/task: task-8: onPostExecute 44850, 9928411
17:26:43.673 E/task: task-9: onPostExecute 44850, 9928698
複製程式碼

我們發現任務確實並行執行了,但是我們統計的計算次數卻不是一百萬次(9928698)了,出現了錯誤,我們這裡不討論這個錯誤出現的原因和怎麼避免,我們更關心的是我們使用的 API 是不是符合我們正常的思維習慣,很顯然這個 API 並不符合
你可能會說了,你看原始碼啊,但是我們先思考一下,一個需要通過閱讀完整文件和閱讀原始碼才能正確使用的 API 真的是個好的 API 嗎?思考完我們再來看一下原始碼,比如這篇文章 《Android 多執行緒:AsyncTask的原理 及其原始碼分析》,看完了有什麼感想麼?這篇文章像其他原始碼分析的文章一樣,用了大量的程式碼片段和極其詳細的程式碼註釋說明原始碼的大概結構和邏輯,但是沒有任何對於原始碼的個人見解,總結 AsyncTask 實現原理的時候說是用兩個執行緒池 + Handler 實現的,但是我們想一下,如果我們不使用 AsyncTask 而是自己封裝一個非同步任務執行的輔助類,我們該怎麼設計?如果任務是序列執行的,我們會用兩個執行緒池去實現嗎?whilefor 迴圈難道不能用麼?佇列不能用麼?既然 AsyncTask 是為了方便主執行緒執行非同步任務的,那我們怎麼避免 AsyncTask 在其他執行緒中建立和執行呢?
我們再來看一下網路請求,Android 有網路請求的 API 嗎?沒有,最開始大家只能用 Java 最原始的 URLConnection 或者 Apache 的 HttpClient 做網路請求,這兩個 API 不但配置複雜使用困難,出現 Bug 的風險也高,而且由於這兩個 API 都沒有提供非同步支援所以還得通過執行緒、執行緒池或者 AsyncTask 等技術才能進行非同步請求,所以各個公司和個人開發者都封裝了自己的一套網路請求 API,或者直接使用 Android-Async-Http 或 Volley 這些別人封裝的,這種情況一直持續到 Square 公司貢獻了優秀的 OkHttp 和 Retrofit,現在幾乎所有公司和個人開發者都在用 OkHttp 做網路請求,也享受著它帶來的便利。現在我們來思考一下,Google 在這方面做了什麼?Google 沒有實力寫出 OkHttp 這樣的庫麼?
像網路請求這種 I/O 密集型的操作很適合用協程去實現,然而 Java 本身不支援協程,就只能用執行緒去寫非同步程式碼了麼?
相對於寫非同步程式碼我們更習慣於寫同步程式碼,但不幸的是我們連 async / await 這樣的關鍵字都沒有

記憶體洩漏

記憶體洩漏是 Android 開發者討論最多的話題之一,為什麼 Android 開發者討論的多?因為寫 Android 程式很容易寫出記憶體洩漏的程式碼,不管是對於新手還是有經驗的開發者

// 錯誤的用例

private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        resultsTextView.setText((String) msg.obj);
    }
};
...
Message message = Message.obtain();
message.obj = "Hello World!";
mHandler.sendMessageDelayed(message, 3000);
複製程式碼
// 錯誤的用例

resultsTextView.postDelayed(new Runnable() {
    @Override
    public void run() {
        resultsTextView.setText(R.string.app_name);
    }
}, 3000);
複製程式碼

像上邊這樣的程式碼看上去沒什麼問題,就是一個文字控制元件 3 秒後顯示一個新的文字,但是在 Android 中卻是一個 “錯誤” 的用例,對於新手來說很容易寫出上面的程式碼,它們可以正常編譯執行且大部分情況下功能良好,如果像上面一樣僅僅設定文字而不是顯示對話方塊甚至不會出現崩潰,所以即使有些情況下出現了記憶體洩漏也察覺不到,除非使用分析工具進行分析
除了上邊兩種用例還有一種常見的錯誤用例:

// 錯誤的用例

resultsTextView.animate().alpha(.5f).start();
複製程式碼

你可能會問了,連執行一個簡單的動畫都會出現記憶體洩漏嗎?是的,在動畫執行結束之前,如果你退出了 Activity,這個 View 的動畫不會被終止,因此這個已經退出的 Activity 也不會被回收
還有一種比較有趣的用例是,在使用單例的時候你無意或者有意引用了 Activity 也會導致記憶體洩漏:

// 錯誤的用例

public class TypefaceManager {

    public static final int FONT_TYPE_ICONIC = 0;
    private volatile static TypefaceManager instance;
    private Context context;

    private TypefaceManager(Context context) {
        this.context = context;
    }

    public static TypefaceManager getInstance(Context context) {
        if (instance == null) {
            synchronized (TypefaceManager.class) {
                if (instance == null) {
                    instance = new TypefaceManager(context);
                }
            }
        }
        return instance;
    }

    public void setTypeface(TextView textView, int fontType) {
        ...
    }
}
...
TypefaceManager.getInstance(MainActivity.this)
        .setTypeface(resultsTextView, TypefaceManager.FONT_TYPE_ICONIC);
複製程式碼

因為單例的生命週期跟應用一樣長,所以當它強引用的 Activity 退出後它依然引用著這個 Activity,導致這個 Activity 即使退出了也無法被回收
其它記憶體洩漏的用例我們就不一一列舉,因為真的很多,我們也意識到,只要稍微不小心就很容易寫出記憶體洩漏的程式碼,就算是有過幾年經驗的開發者也可能依然寫著 new Thread().start() 這樣的程式碼,但我們不能把所有的責任都推給開發者,我們思考一下,如果 API 設計的合理一點、編譯器的程式碼檢測更智慧一點,可以避免多少常見的記憶體洩漏程式碼?

設計缺陷

Android 系統最受人詬病的問題就是卡,為什麼 iOS 那麼流暢而 Android 這麼卡頓呢?卡頓的原因有很多,直接原因可能是硬體效能低或者開發者水平參差不齊寫出來的應用卡,但根本原因我覺得就是 Android 的設計缺陷問題,思考一下,為什麼系統的應用或者 Google 的應用相對來說就很流暢呢?
就像我們上面討論的那樣,非同步困難加上很容易寫出記憶體洩漏的程式碼讓應用的質量很難保證,即使我們認認真真費盡力氣地管理資源(如在 onDestroy() 生命週期方法中停止所有動畫的執行、停止所有的網路請求、登出監聽器、釋放暫時不用的資源)也可能因為其他的原因導致應用卡頓,如過度繪製、佈局層級深、序列化複雜物件、建立多個重量級物件,記憶體佔用過高、頻繁建立回收資源引發的 GC 等等都可能導致應用產生卡頓,而只有豐富經驗的開發者才可能在這些方面做得很好,寫出來的應用才可能很流暢
Google 也意識到了這些,所以給 Android(或者說是 SDK)打了個補丁,還給它取了個名字,叫 Jetpack:

Jetpack is a suite of libraries, tools, and guidance to help developers write high-quality apps easier. These components help you follow best practices, free you from writing boilerplate code, and simplify complex tasks, so you can focus on the code you care about.

在 Jetpack 中 Google 提供了一些工具可以讓開發者不再很容易寫出記憶體洩漏和卡頓的程式碼了,也就是說,開發者只要使用 Jetpack 就基本可以寫出不卡頓的高質量應用了
Jetpack 中確實提供了很多很基本很有趣甚至很優秀的實現,如 LiveData 不但實現了像 Rx 一樣的可觀察資料來源,還可以自動跟觀察者(Activity/Fragment)的生命週期繫結,ViewModel 讓 Android 的 MVVM 變為可能,Data Binding 讓資料驅動檢視的思想變為可能,Lifecycle 讓我們可以從臃腫的生命週期方法中解脫出來,Room 讓我們可以方便且安全地持久化資料
Jetpack 確實有很多優點,但並不完美,你可以使用它也可以不使用它,它的學習成本也很高,很多人排斥使用 Data Binding,因為佈局的 XML 檔案和原始碼的 Java 檔案離的太遠了,XML 檔案中也可能包含簡單的業務程式碼,所以一個業務邏輯可能需要同時閱讀這些檔案才能知道詳細的資訊,程式碼可讀性可能會降低,這在一些開發者看來是無法接受的

下一個十年

Android 的首個十年已經過去了,歷史也證明了它是個成功的移動作業系統,這要歸功於它的開放和自由,歸功於無數的 Android 開發者為它開發的應用,歸功於手機廠商們對它的支援,下一個十年,Android 系統依然會是除了 iOS 外最受歡迎的作業系統。但是下下個十年,下下下個十年它還會是嗎?從技術上來說沒有比它更優秀的移動作業系統嗎?
你可能會說了,一個成功的作業系統光從技術上優秀是遠遠不夠的,是這樣的,Windows Phone 就是最好的例子,甚至連 Google 自己都無法馬上用新的作業系統取代 Android 作業系統。但是歷史總是在進步的,技術在進步,人們的需求在提高,上個世紀的語言 Java 語言越來越難以滿足開發者尤其是 Android 開發者的需要,所以 Google 和開發者很想逐漸用新的語言(如 Kotlin)替代它,就像 Swift 替代 OC 一樣,而 Android 作業系統亦是如此,Google 難道沒有意識到 Android 的設計缺陷嗎?Google 難道沒有想過用新的作業系統替代 Android 嗎?
你可能已經想到了,Flutter 啊,Flutter 不是作業系統,它是一個 UI 框架,一個 Fuchsia 作業系統使用的 UI 框架,而 Google 對於正在研發的 Fuchsia 作業系統一直很低調,它的核心採用的是微核心計劃中的一個名字叫 Zircon 的微核心,是一個對硬體要求很低的高效核心,一個非類 UNIX 的全新核心,核心原始碼的提交最近幾年也越來越頻繁。Flutter 可以寫 Android 和 iOS 應用,雖然看起來像 React 一樣是個跨平臺的框架,但是卻有幾分兵馬未動糧草先行的味道

思考

幾年前剛自學幾個月 Java 和 Android 的我就使用了它參加了比賽,寫了第一個讓我很有成就感的應用,寫了我的第一篇技術部落格,直到現在,我依舊享受著開發的 Android 應用帶給我的成就感,帶給我的一切。然而技術之路尤其是 Android 技術之路向來就不平坦,經歷過 Eclipse 安裝 ADT 外掛的艱難,經歷過十幾分鍾才能啟動且嚴重卡頓的 Android 模擬器,經歷過修改一行程式碼需要編譯幾分鐘的煎熬,經歷過適配各個機型 ROM 的痛苦,經歷過進階的迷茫,經歷過莫名其妙的系統 Bug 的無奈
無論如何,希望以後依然能夠保持對技術的熱情,保持對技術的寬容,更重要的是保持對生活的熱愛,願出走半生,歸來仍是少年

相關文章