Android 學習筆記思考篇

概述

Android 系統從 2008 年正式發佈到如今已通過去了 10 年,系統版本也來到了 9,做爲開發者,或者做爲用戶,咱們見證了系統一次次大大小小的改動,見證了系統的不斷完善,見證了咱們寫的每一個 Android 小程序給咱們帶來的成就感。可是,當咱們寫的程序愈來愈多時,當咱們對 Android 應用開發愈來愈瞭解時,咱們發現它並不完美,甚至有些簡陋:
Service 從字面上理解就是後臺服務,一個看不見的服務不該該運行在後臺嗎?不該該運行在獨立的進程中嗎?就算運行在主進程中那不該該運行在後臺線程中嗎?
文檔中確實提醒過不要在主線程中進行耗時操做,那爲何在主線程中讀寫文件沒有問題?甚至連警告都沒有?讀寫 SharedPreferences 文件算不算讀寫文件?算不算耗時操做?
把耗時操做放在後臺線程中執行,那意味着咱們須要精通 JUC?須要建立線程,維護線程,把線程變成什麼 Looper 線程才能用 Handler 通訊,還得考慮線程安全,什麼?爲了性能和防止無限建立線程引起問題還要了解並使用線程池技術?用線程池就不會有問題了麼?咱們能不能不關心線程、線程池、LooperHandler 什麼的,咱們就是想單純地讓這段代碼異步執行而已,奧,原來有 AsyncTask 就不用關心這些了啊,那咱們還須要維護這些 AsyncTask 嗎?這些異步任務的生命週期能跟視圖組件綁定嗎?不能的話怎麼手動維護這些 AsyncTask 啊?
異步任務執行完以後咱們想直接顯示個對話框行不行?什麼?得先判斷 Activity 的狀態才能顯示?不判斷好像也沒什麼問題啊?退出 Activity 的時候還須要手動關閉各類對話框?不關閉好像也沒什麼問題啊?java

異步

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 程序很容易寫出內存泄漏的代碼,無論是對於新手仍是有經驗的開發者app

// 錯誤的用例

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 設計的合理一點、編譯器的代碼檢測更智能一點,能夠避免多少常見的內存泄漏代碼?async

設計缺陷

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 的無奈 不管如何,但願之後依然可以保持對技術的熱情,保持對技術的寬容,更重要的是保持對生活的熱愛,願出走半生,歸來還是少年

相關文章
相關標籤/搜索