《Android開發(fā)藝術探索》筆記二

第十章:Android的消息機制
  1. Handler是Android消息機制的上層接口,開發(fā)人員只需要與它交互即可,底層需要Looper與MessageQueue的支持,MessageQueue是單鏈表數(shù)據(jù)結構存儲Message,Looper存儲在ThreadLocal中,與線程關聯(lián),三者配合完成指定邏輯在指定線程的順序執(zhí)行。
  2. Handler是在某一個線程中創(chuàng)建的(并提前完成了消息的實現(xiàn)邏輯),然后在其他線程中去發(fā)送消息,原線程的Looper執(zhí)行到該消息會調用Handler的消息處理邏輯(在原線程),因此Handler可以輕松的實現(xiàn)將業(yè)務邏輯在指定線程中去調用,最常見的就是更新UI的業(yè)務邏輯在子線程觸發(fā),在主線程處理。
  3. 由于控件的更新不是線程安全的,因此要使用主線程Handler去更新UI,使得更新邏輯在單線程順序執(zhí)行,避免了并發(fā)帶來的問題。PS:處理并發(fā)也可以在控件的更新方法上synchronized加鎖,但是效率低,因此采用單線程處理,類似Java并發(fā)中的阻塞隊列。
  4. 線程默認是沒有Looper的,創(chuàng)建Handler時也會進行檢查,沒有Looper的話會拋出異常,因此在子線程使用Handler要先自己創(chuàng)建Looper并開啟循環(huán);主線程默認已有Looper且已啟動了Looper.loop(),因此主線程可以直接創(chuàng)建Handler,通過Looper.getMainLooper()可直接獲取到主線程Looper。
  5. Handler、Looper、MessageQueue大致工作流程:
    一個線程只能創(chuàng)建一個Looper,業(yè)務過多可以創(chuàng)建多個Handler;Looper內部創(chuàng)建并管理一個messageQueue,線程中創(chuàng)建Handler時也會檢查是否已經含有Looper(主要為了與其中的messageQueue建立關聯(lián)),然后looper.loop()方法會不斷循環(huán)messageQueue去取消息,messageQueue的next()方法取消息是阻塞的,因此looper無限循環(huán)取調用,是阻塞的;其他線程想要執(zhí)行指定邏輯到創(chuàng)建Handler的線程時,就可以調用Handler的send()方法或post()方法,最終消息會被Handler發(fā)到其內部的messageQueue中,然后Looper會單線程順序拿取里面的消息同時再調msg.target(其實就是handler)的dispatchMessage()方法,dispatchMessage()方法是Handler里的,最終會被下發(fā)到handleMessage()方法中,也就是最初創(chuàng)建Handler時實現(xiàn)的重寫方法,這樣一來消息就是提前在創(chuàng)建者中實現(xiàn),然后在調用者中去觸發(fā),完成將指定好的邏輯放到指定線程中執(zhí)行的功能。
  6. ThreadLocal的工作原理:
    一般而言線程是內存數(shù)據(jù)共享的,ThreadLocal是個類似全局數(shù)據(jù)線程作用域管理類,可以輕松實現(xiàn)不同線程存取不同的數(shù)據(jù)副本,Looper就是一個需要每個線程獨享的數(shù)據(jù),當然實現(xiàn)類似方式也可用對象一級一級傳遞下去(始終保持同一個引用),或者全局維護靜態(tài)單例對象等;ThreadLocal有個內部類ThreadLocalMap(不是Map,里面有個table數(shù)組),里面有個弱引用Entry包裝類(弱引用ThreadLocal自己),包裝了value(具體數(shù)據(jù)),table數(shù)組存著這些Entry,每個線程有個threadLocals數(shù)據(jù)域(ThreadLocalMap類型);當使用ThreadLocal存數(shù)據(jù)時,set()方法會根據(jù)線程去取其中的threadLocals,然后將ThreadLocal作為key去尋找放入位置,創(chuàng)建Entry對象包裝value并放入table數(shù)組;當取數(shù)據(jù)時,get()方法會根據(jù)線程去取其中的threadLocals,然后將ThreadLocal作為key去table數(shù)組中找到其中的Entry,然后拿到Entry的value返回數(shù)據(jù);這里ThreadLocal作為key主要是通過hash等去找出要存儲的位置下標,table是個數(shù)組,將生成的Entry包裝類放入數(shù)組指定的位置。
  7. MessageQueue的工作原理:
    MessageQueue叫消息隊列,但本身的實現(xiàn)并不是隊列,是一個單鏈表的數(shù)據(jù)結構,在插入和刪除上比較有優(yōu)勢(不過順序插入刪除沒啥體現(xiàn)吧);它只有插入enqueueMessage()和讀取next()兩個操作,讀取伴隨著刪除,next()方法是阻塞的。
  8. Looper的工作原理:
    Looper.prepare()方法會創(chuàng)建一個looper,內部創(chuàng)建并管理一個messageQueue,Looper.loop()方法會執(zhí)行for循環(huán)無限循環(huán)消息隊列去取消息,由于消息隊列的next()方法是阻塞的,因此它的loop()方法也是阻塞的;由于主線程比較特殊,Looper提供了getMainLooper()方法可以在任何地方獲取到主線程的Looper對象;Looper提供了quit()quitSafely()方法用于退出Looper,類似線程池的shutdown()方法,前者立即退出,后者等消息隊列處理完后退出,退出后Handler的send()方法會返回false,在子線程中我們自己創(chuàng)建的Looper在不再使用時最好手動quit()一下終止循環(huán);當Looper標記成退出狀態(tài)后,messageQueue的next()方法就會返回null,for循環(huán)就會return退出。
  9. Handler的工作原理:
    Handler就是發(fā)送和處理消息,發(fā)送使消息順序執(zhí)行,處理使任務到指定線程中執(zhí)行,發(fā)送消息時直接調用queue.enqueueMessag()將消息插入隊列,查詢方法由Looper來調,是阻塞的;消息分發(fā)判斷順序為post發(fā)出的Runnable > 創(chuàng)建Handler的callback > Handler的handleMessage();Handler其實就是提前包裝好業(yè)務邏輯,由其他線程發(fā)起消息,然后原線程Looper循環(huán)到消息后調用Handler來處理消息;Handler的構造方法中可以直接傳入一個Looper,就可以指定消息發(fā)送到該線程處理了,HandlerThread就是一個自帶Looper的線程,提供出Looper來給外部創(chuàng)建Handler使用,實現(xiàn)單線程消息循環(huán)機制。
  10. Android的主線程就是ActivityThread,主線程入口方法為main()方法,內部會Looper.prepareMainLooper()來創(chuàng)建主線程的Looper以及MessageQueue,并通過Looper.loop()來開啟主線程消息循環(huán),ActivityThread中的H用來給AMS的Binder回調回來提交到主線程做一些回調使用。
    主線程的消息循環(huán)需要的Handler就是ActivityThread.H。
  11. 兩個問題:
    ? 主線程Looper的死循環(huán)為何不會導致卡死:循環(huán)里queue.next()會阻塞,所以死循環(huán)并不會一直執(zhí)行,相反的,大部分時間是沒有消息的,所以主線程大多數(shù)時候都是處于休眠狀態(tài),也就不會消耗太多的CPU資源導致卡死;阻塞的原理是使用Linux的管道機制實現(xiàn)的,主線程沒有消息處理時阻塞在管道的讀端,Binder線程會往主線程消息隊列里添加消息,然后往管道寫端寫一個字節(jié),這樣就能喚醒主線程從管道讀端返回,也就是說looper循環(huán)里queue.next()會調用返回。
    ? 主線程的死循環(huán)阻塞了如何處理其它事務:在Looper.prepareMainLooper()Looper.loop()之間創(chuàng)建了ActivityThread并thread.attach(false),已經將主線程的Handler放出去了,自己的代碼邏輯都是通過H來驅動執(zhí)行的,只需要通過binder線程向H發(fā)送消息即可,所以說Android應用程序也是依靠消息驅動來工作的。
第十一章:Android的線程和線程池
  1. 線程是操作系統(tǒng)調度的最小單元,同時也是一種有限的系統(tǒng)資源,不可無限制的創(chuàng)建,線程不可能做到絕對的并行,除非線程數(shù)小于等于CPU的核心數(shù),一般來說是不會的。
  2. 主線程指進程鎖擁有的線程,與Java一樣,進程一開始就有一個線程(主線程或UI線程),主線程要保持較高的響應速度(與用戶前臺交互),因此不能有太多的耗時操作,主線程以外都是子線程,子線程也叫工作線程,用于異步執(zhí)行耗時操作。
  3. Andorid中的幾種線程形態(tài):
    ①AsyncTask:
    ? 輕量級異步任務類,底層使用線程池+Handler實現(xiàn),提供了幾種方法回調(不同線程中),方便開發(fā)者在子線程中做耗時任務后更新UI。
    ? 幾點限制:必須主線程加載(4.1及以上版本已經被系統(tǒng)自動完成);必須在主線程創(chuàng)建(內部的Handler是static的,類加載時就會創(chuàng)建,因此必須在主線程);excute方法必須在主線程調用;一個AsyncTask只能執(zhí)行一次,后續(xù)會拋異常。
    ? 1.6之前任務是串行執(zhí)行,1.6~3.0使用線程池并發(fā)執(zhí)行,3.0以又是串行執(zhí)行,但可以通過executeOnExecutor()來強制并行執(zhí)行;因此3.0以前最大128個線程并發(fā),10個緩沖隊列,多了就默認拒絕策略拋異常了;3.0以后用一個Deque包裝Runnable,循環(huán)取消息并執(zhí)行,因此進程中的所有AsyncTask都在這串行線程池中排隊執(zhí)行。
    ? 不建議執(zhí)行特別耗時的任務,特別耗時的任務建議使用線程池來執(zhí)行。
    ②HandlerThread:
    ? 內部創(chuàng)建了Looper并開啟了循環(huán),可以在外部獲取此Looper來創(chuàng)建Handler,將任務封裝到外部來調用,在該Thread中順序執(zhí)行,類似主線程消息機制。
    ? 由于內部Looper是無限循環(huán)的,使用完畢記得調用quit()quitSafely()一下來終止線程。
    ③IntentService:
    ? 本身是個服務,內部有一個worker(HandlerThread)線程可以執(zhí)行耗時任務,當onHnadleIntent()方法執(zhí)行結束后,它會自動stopSelf(id)來結束自己,stopSelf()會立即停止服務,stopSelf(id)會等所有消息處理完畢后停止服務;每次啟動一次服務,都會向Handler發(fā)出一條message,多個任務順序執(zhí)行。
    ? 服務的優(yōu)先級更高一些,如果是一個后臺線程,沒有四大組件的依賴在內存不足時容易被殺死,而服務的優(yōu)先級較高,這種在服務中再開啟線程執(zhí)行任務,任務線程則不易被殺死。
  4. 線程池相關:
    ? 作用:重用線程、有效控制最大并發(fā)數(shù)、簡單的線程管理。
    ? 構造方法7個參數(shù):corePooSize(核心線程)、maximumPoolSize(最大線程)、keepAliveTime(空閑線程存活時間)、unit(時間單位)、workQueue(阻塞隊列)、threadFactory(線程創(chuàng)建工廠)、rejectedExecutionHandler(拒絕策略)。
    ? 拒絕策略:AbortPolicy(拋異常 默認)、CallerRunsPolicy(在調用者線程直接用)、DiscardPolicy(拒絕不拋異常)、DiscardOldestPolicy(將最早任務刪掉加入任務隊列)。
    ? 內部調用順序:核心線程 > 任務隊列 > 最大線程 > 拒絕策略。
  5. 四種常見線程池:
    ? FixedThreadPool:核心數(shù)=最大數(shù),0存活時間;核心線程不會被回收,適用于快速處理任務。
    ? SingleThreadExecutor:核心數(shù)=最大數(shù)=1,0存活時間;特殊的Fix線程池,單線程順序執(zhí)行,不需要處理同步問題。
    ? CachedThreadPool:核心數(shù)=0,最大數(shù)=MAX,存活時間60s,無任務隊列;適用于大量短任務,有空閑的直接用,沒有就創(chuàng)建。
    ? ScheduledThreadPool:核心=自定義固定值,最大=MAX,0存活時間;適用于固定周期的重復任務執(zhí)行。

注意:
主線程方法體中使用handler的post()方法,由于同在主線程,當前方法體執(zhí)行完后才會可能開始執(zhí)行msgQueue隊列中的下一個消息;而thread.start()之后子線程就處于準備狀態(tài)要開始并行執(zhí)行了,與當前方法體下一行代碼的執(zhí)行順序是不可控的(不過一般在子線程做的都是耗時的邏輯,不會那么快跑完)。

  1. 多線程相關類:
    ? Runnable可以在線程或線程池中執(zhí)行,Callable、Future、FutureTask只能在線程池中執(zhí)行。
    ? Callable:也是一個業(yè)務包裝類,包裝到call方法,有返回值<T>。
    ? Future:本身是一個接口,封裝了cancel()、isDone()、get()等方法,get()是等待業(yè)務返回值(阻塞方法)。
    ? FutureTask:實現(xiàn)了RunnableFuture<V>接口,RunnableFuture<V>繼承自Future和Runnable接口,算是一個接口適配器模式(類似Stub類),可以把一個Runnable、Callable或FutureTask來submit()給線程池,完了拿到Future<?>后可以對任務進行管理和獲取返回值(阻塞)。
  2. 程序中的優(yōu)化策略:
    ? CopyOnWriteArrayList:讀寫List,基于讀寫分離思想,可并發(fā)讀,寫的時候copy了一份副本,因此寫占用了雙倍內存,用的少。
    ? ConcurrentHaspMap:分段鎖HashMap,分段加鎖技術,可以實現(xiàn)并發(fā)HashMap。
    ? BlockingQueue:
    add()/move():可用返回true/false,不可用拋異常。
    offer()/peek():可用返回true/false,不可用返回null,不阻塞。
    pull(time,unit)/element():等待取出隊首元素;拋異常。
    put()/take():阻塞方法。
    ArrayBlockQueue(數(shù)組實現(xiàn)/有界/線程安全/阻塞隊列)、LinkedBlockQueue(單向鏈表實現(xiàn)/先進先出/無界/線程安全/阻塞隊列);LinkedBlockDeque(雙向列表實現(xiàn)/容量可指定也可不指定/線程安全/阻塞隊列)、ConcurrentLinkedQueue(鏈接節(jié)點實現(xiàn)/線程安全/無界)。
  3. 同步鎖技術:
    ? synchronized:粗略鎖,作用于對象、方法或class,利用對象的內部鎖;當作用于函數(shù)時,鎖的是該對象中所有同步實例方法,當作用于static函數(shù)時,鎖的是該類的所有同步靜態(tài)方法;方法加鎖鎖的是整段業(yè)務邏輯;在拿到鎖后,sleep()方法不會改變鎖的情況。
    ? ReentrankLock/Condition:每個對象有內部鎖,也可以new一個條件Lock,它具有靈活性、可通信性,可以實現(xiàn)輪訓和定時鎖,利用Condition可以實現(xiàn)線程間條件通信。
    ? Semaphore:信號量,本質是一個共享鎖,維護一個信號量許可集,自定義許可集大小,其余線程可以調用smp.aquire()/release()方法來獲取和釋放鎖,用于控制線程通過量。
    ? CyclicBarrier:循環(huán)柵欄,自定義等待集大小和業(yè)務Runnable(),其余線程調用cb.await()進入等待,等待線程夠數(shù)時執(zhí)行定義好的業(yè)務run()方法,完了線程繼續(xù)執(zhí)行。
    ? CountDownLatch:同步輔助類,計時等待,自定義等待次數(shù),本線程調用cd.await()進入等待,其余線程執(zhí)行完調用cd.countDown()等待計數(shù)減一,等待次數(shù)結束本線程繼續(xù)執(zhí)行。
第十二章:Bitmap的加載和Cache
  1. 比較常用的緩存策略是內存緩存LruCache和磁盤緩存DiskLruCache,這種緩存策略為:當緩存快滿時,會淘汰近期最少是用的緩存目標。
  2. Bitmap的高效加載是先將BitmapFactory.Options的inJustDecodeBounds設為true,這樣不會真正的加載圖片,只是獲取圖片的原始寬高,然后計算出縮略比例設置inSampleSize參數(shù),完了再將inJustDecodeBounds設為false,最后再用Factory加載出真正的Bitmap就行了。注意:采樣率一般設為2的倍數(shù),計算時一般也是先用長寬/2,如果還大再去壓縮,避免過度壓縮,具體算法的話因人而異。
  3. LruCache是一個泛型類,內部采用LinkedHashMap存儲緩存對象的強引用,當內存不足時,會回收最近最少使用目標,內部已經實現(xiàn)了線程安全;DiskLruCache將緩存對象寫入文件系統(tǒng),目錄默認是sdcard/Andorid/data/package_name/cache下,也可根據(jù)需求自定義目錄。也可使用ACache進行緩存,類似SP文件。
  4. 四種引用類型:強引用(直接的對象引用)、軟引用(系統(tǒng)內存不足時會被gc回收)、弱引用(只要被gc掃到就會回收)、虛引用(結合引用隊列使用)。
  5. Runtime.getRuntime().maxMemory()可獲取系統(tǒng)運行最大內存大??;Runtime.getRuntime().availableProcessors()可獲取CPU核心數(shù)。
  6. 優(yōu)化列表卡頓的幾種做法:異步加載圖片、控制加載頻率、滑動停止加載、開啟硬件加速。
第十三章:綜合技術
  1. 異常處理器:
    ? 程序運行時如果有未捕獲異常則會崩潰,這時候如果用setDefaultUncaughtExceptionHandler()給線程設置一個異常處理器CrashHandler,當程序崩潰時就會回調uncaughtException()方法。
    ? 默認的異常處理器是Thread的靜態(tài)成員,因此它作用于當前進程的所有線程(也可給線程設置單獨的異常處理器),我們一般是先獲取到默認的異常處理器,再設置自己的異常處理器,當崩潰時自己先上傳log,然后繼續(xù)讓默認異常處理器去終止進程。
  2. APK分包:
    ? Android中單個dex文件能包含的最大方法數(shù)為65536,包括AndroidFramework、依賴的jar包和本身的所有代碼,超數(shù)后無法編譯通過且拋出異常;有時候方法數(shù)并沒有到65536,能編譯通過但是低版本手機不能安裝,這是因為應用在安裝時,dexopt程序會為了優(yōu)化apk,將dex文件的所有方法信息提前放到一個固定大小的方法緩沖區(qū)LinearAlloc內,不同版本的緩沖區(qū)大小是不同的,超過了就裝不上。
    ? Android5.0以前使用multidex需要引入android-support-multidex.jar包,5.0以后默認支持了multidex,注意BuildTools要升級到21.1以上。
    ? 在gradle文件中的defaultConfig里開啟分包multiDexEnabled true,引入jar包,manifest中將application標簽設為MultiDexApplication或 Application繼承自MultiDexApplication,在attachBaseContex()中設置MultiDex.install(this)即可。
    ? gradle里可以寫腳本--main-dex-list配置哪些類打到主dex中,注意multidex的jar包中9個類必須打進主dex,Application的成員變量是先于attachBaseContex()方法的,因此不要提前聲明其他dex中的類。
  3. dex2jar和jd-jui可以查看apk文件解壓后的dex中的java代碼;
    apktool可以對apk進行解包、修改和二次打包,二次打包后需要重新簽名。
    ? 解包:apktool.bat d -f xxx.apk yyy
    ? 打包:apktool.bat b yyy xxx2.apk
    ? 簽名:java -jar signapk.jar testkey.x509.pem testkey.pk8 xxx2.apk xxx2_signed.apk
  4. 簽名、證書相關:
    ? .keystore/.jks/.pem/.pk8是同級的,都是簽名文件,都可以給apk簽名。
    ? keytool:是個秘鑰和證書管理工具,可以用來生成秘鑰庫,如Eclipse生成的是.keystore,AS生成的是.jks,內部可按別名存多組密鑰的信息。
    ? jarsigner:工具利用密鑰庫中的信息來校驗java存檔文件的數(shù)字簽名和簽名jar/apk文件用的。
    ? keytool用法:
    keytool生成證書:keytool -genkey -keystore xxx.keystore -alias xxx -keyals RSA -validity 1000;
    keytool查看證書:test keytool -list -keystore test.keystore;
    jarsigner簽名apk:jarsigner -verbose -keystrore xxx.keystore -signerjar signed.apk unsign.apk 'xxx';

? .jks是一個秘鑰管理倉庫,只存放一類東西(密鑰),庫中的密鑰類型分為私鑰、公鑰和密鑰對,都有別名,公鑰進入倉庫就能拿走,私鑰是需要密碼的。
? .keystore是Java下keytool生成的秘鑰管理倉庫,存放兩類東西(密鑰實體和證書),將密鑰實體和證書信息存在keystore中(證書只有公鑰)。

  1. ClassLoader動態(tài)加載過程:
    ? Dalvik虛擬機和其他Java虛擬機一樣,在程序運行時首先要將類加載到內存中,類可以從class文件中讀取也可以從其他形式的二進制流中讀取。
    ? Andorid平臺上虛擬機運行的是Dex字節(jié)碼,一種對class文件優(yōu)化后的產物,Android將所有class文件合并優(yōu)化,生成xxx.dex,如果分包的話會有多個dex文件。
    ? ClassLoader分類:Classloader -> BootClassLoader、UrlClassLoader、BaseDexClassLoader(DexPathList) -> PathClassLoader(只能加載已安裝的apk,art虛擬機還是可以加載未安裝的)、DexClassLoader(支持其他apk、dex和jar文件)。
    ? ClassLoader構造方法中傳入父Classloader,無的話默認創(chuàng)建PathClassLoader -> BootClassLoader,找類的時候先判斷是否被加載過,然后先從父loader中加載(雙親委派機制)。
    ? BootClassLoader是ClassLoader內部類我們沒法使用,UrlClassLoader只能加載jar 不能用(dalvik虛擬機不能直接識別jar),BaseDexClassLoader我們一般修改這個類;BaseDexClassLoader(dexPath,optimizedDirectory,libraryPath,parentCl)四個參數(shù)為:目標dex文件路徑、odex優(yōu)化后的dex存放路徑、c/c++庫的路徑、父classloader(一般context.getClassLoader())。
    ? 類加載過程會從pathList中尋找class,DexPathList中dex包裝成Element存在一個數(shù)組里(一般只有一個element),dexElement中的dexFile存著最終的dex文件,因此將新的dex文件插入dexElement數(shù)組前面可以實現(xiàn)熱更新。
    ? Android5.0以前是Dalvik虛擬機,5.0以后是Art虛擬機,據(jù)說Art在程序第一次安裝時就把字節(jié)碼預先編譯成機器碼(預編譯)并優(yōu)化了性能等細節(jié)(將.dex轉為oat文件),接口是一致的,實現(xiàn)了一套完全兼容Java虛擬機的接口。
第十四章:JNI和NDK編程
  1. JNI是Java Native Interface(Java本地接口),為了方便Java調用C、C++層的一層本地封裝接口,NDK是Android提供的一套工具集合,方便通過JNI來訪問本地代碼和編譯生成各版本的so庫。
  2. JNI編程的好處是提高安全性難以反編譯、動態(tài)庫便于平臺移植和提供某些計算類代碼執(zhí)行效率等。
  3. C層JNIEnv*是一個指向JNI環(huán)境的指針,類似Java里的this,可以通過它訪問JNI提供的接口方法;jobject是Java層的引用(Java對象的this),可以通過它訪問Java層代碼,類似回調;JNIEXPORT和JNICALL都是JNI定義的宏。
  4. JNI開發(fā)流程:定義native方法 -> System.loadLibrary()加載生成的.so庫 -> javac/javah生成頭文件 -> 實現(xiàn).cpp/.c文件 -> gcc編譯出.so庫 -> java -D運行類即可。
  5. NDK提供了一系列工具集合便于生成.so庫和調用.so庫,Eclipse中需要配置.mk文件,AS中直接在gradle中配置即可,ndk-build命令可以導出.so庫,就可以用了,創(chuàng)建so庫除了使用ndk-build命令,也可使用gradle自動編譯產生so庫。
  6. JNI數(shù)據(jù)類型分為基本類型和引用類型;基本類型以j開頭(jint),引用類型有類、對象和數(shù)組,以j開頭(jobject)。
  7. JNI類型標簽標識了一個特定的Java類型,可以是類和方法也可以是數(shù)據(jù)類型;
    ? 類:L+包名+類名+;,如Ljava/lang/String;
    ? 基本類型:大寫字母,如int為I;
    ? 數(shù)組:[+大寫字母/類標簽,如[D/、[Ljava/lang/String;
    ? 方法:參數(shù)類型+返回值,如boolean fun(int a, double b, int[]c)為(ID[I)Z。
  8. JNI調Java層方法時,會先找類,再找方法的id,然后通過id來調方法(如果是非靜態(tài)方法要先構造類對象)。
第十五章:Andorid性能優(yōu)化
  1. 布局優(yōu)化:多用LinearLayout、FrameLayout;<include/>標簽(id覆蓋,width/height都指定其余屬性才生效)、<merge/>標簽(什么屬性都沒有)、<ViewStub/>標簽(加載后就沒stub這個對象了,用inflatedId或生成的View)。
  2. 繪制優(yōu)化:onDraw()方法由于會頻繁調用,因此不要創(chuàng)建過多局部變量、不要做耗時操作等,該方法在主線程執(zhí)行;屬性動畫、補間動畫等在頁面onDestroy()或View的onDetachedFromWindow()后記得關閉。
  3. 內存泄露優(yōu)化:無論何時靜態(tài)變量不要持有activity的引用、靜態(tài)內部類+弱引用、流的關閉、監(jiān)聽者的解注冊等。
  4. 線程優(yōu)化:ANR發(fā)生時在/data/anr目錄下會有一個traces.txt文件記錄log。
    注意:盡量不要讓主線程也去與子線程競爭鎖,比如都去訪問synchronized方法,如果鎖被其他子線程持有且耗時,主線程就會阻塞住。
  5. ListView和Bitmap優(yōu)化:getView()不要做耗時操作,開啟硬件加速;Options采樣壓縮等。
  6. 其他優(yōu)化:使用線程池、不要過多使用枚舉、常量使用final修飾、SparseArray等數(shù)據(jù)結構、適當軟引用和弱引用、靜態(tài)內部類避免持有外部類引用等。
補充知識點
  • SDK區(qū)別:
    ? minSDKVersion <= targetSDKVersion <= compileSDKVersion
    ? minSDKVersion:要求的最低API版本(安卓系統(tǒng));
    ? targetSDKVersion:程序適配的API版本(在這個版本的系統(tǒng)上運行效率高一些,權限等功能會體現(xiàn)出來);
    ? compileSDKVersion:代碼編譯時的API版本(提示方法的更新、棄用等);
    注意:每一次安卓系統(tǒng)的發(fā)布都會伴隨新的SDK和support庫一起發(fā)布,support庫的大版本號需要和compileSDKVersion保持一致(只是官方建議,不一致也能編譯通過),有些類只在高版本SDK里有,所以如果compileSDKVersion編譯時判斷通過則編譯打包沒事,但是運行時在低版本可能就會崩潰了。
    ? 因此才會有SupportV4、V7等支持包,包含了如Fragment、Material Design控件等兼容包,引入V7默認也會引入V4下的所有類,同時引入也不會報錯。

  • Jar包沖突的本質:
    ? 問題:Java應用程序因某種因素,加載到不正確的類而導致其行為跟預期不一致。
    ? 兩種情況:同一個Jar包出現(xiàn)了多個不同版本;不同Jar包內含有包名類名相同的類。
    ? 解決方法:對于第一類,maven和gradle有一套自己的處理邏輯可以選擇出較高的版本進行編譯,但是如果不符合預期就還是有可能報錯;對于第二類,如果類的包名、類名、方法信息完全一致編譯就不會報錯,classloader會選出靠前的jar包中的類,但是運行時可能會因為功能不對而達不到預期或者報錯。
    ? 安卓中:相同的V4包重復引入,只要相同方式但版本不同,不會報錯,會自動選擇最高版本;如果是maven和本地jar同時映引入但版本相同,也不會報錯;只有maven和本地jar同時引入切版本不同才會報錯;如果是多module打包,對于庫的引入可以使用exclude來避免重復。

  • 安卓抓包工具:
    Fidder,必須在同一網段內可抓包,PC默認監(jiān)聽127.0.0.1:8888端口,手機連接PC代理;捕獲手機Https請求時,需要手機訪問PC的IP并下載cer證書,有些手機下載需完要到安全設置->從文件夾讀取證書來手動安裝cer證書。
    功能:捕獲Https、Http統(tǒng)計、命令行、斷點break point、AutoResponder返回本地文件、配置Host、過濾會話Filters、會話比較、會話查詢、會話保存、編碼工具等。

  • getMeasuredWidth()和getWith()的區(qū)別:
    ? getMeasuredWidth()方法獲取的值是setMeasuredDimension()中設置的值,它是在measure()方法后確定的;getWidth()方法獲取的值mRight-mLeft,它是在layout()方法后確定的,因此比getMeasuredWidth()值獲取的晚。
    ? 這兩個值其實沒什么不同,一般在onLayout()方法中可以使用getMeasuredWidth()方法獲取寬度,而在onLayout()之后使用getWidth()就行了。
    注意:在Activity的onWindowFocusChanged()、或view.post()方法后view的getWidth()才被賦值。

  • LayoutInfalter三個方法的區(qū)別:
    ? Inflate(resId,null):只創(chuàng)建temp,返回temp,外層寬高無效;
    ? Inflate(resId,parent,false):創(chuàng)建temp,然后執(zhí)行temp.setLayoutParams(params),返回temp,外層寬高有效(ListView中也有效,用于getView()方法獲取單獨的View);
    ? Inflate(resId,parent,true):創(chuàng)建temp,然后執(zhí)行root.addView(temp, params),最后返回root,外層寬高有效(ListView中會報錯,ListView不允許直接addView(),正常布局沒事)。

  • WebViewClient方法加載順序:
    ? 如果沒有設置WebViewClient則會打開默認瀏覽器;設置后如果返回true,則表示代碼處理url,webView不處理,因此后面一般再跟一句webView.load(url);如果直接返回false,表示由webView來處理該url,不用寫額外代碼。
    ? onReceiveTitle()每次都會執(zhí)行,包括goBack()后也會執(zhí)行,重定向時title可能會有問題,需要自己維持title棧和url棧的來解決重定向問題。
    第一頁:onPageStarted() -> onPageFinished();如果無連接則onPageStarted() -> onReceivedError -> onPageFinished()。
    第二頁:shouldOverrideUrlLoading() -> onPageStarted() -> onPageFinished();無連接時同上。
    返回上一頁:onPageStarted() -> onPageFinished();返回上一頁不調overrideUrl,無網絡連接不影響返回上一頁。
    注意:onReceivedError和shouldOverrideUrlLoading兩個被棄用的方法,有時候不會被回調,重寫新方法即可(情況處理更多樣),但是新方法回調順序可能有變,如onPageStarted() -> onPageFinished() -> onReceivedError()。

  • 熱修復原理:
    ①DEX分包方案:
    ? JVM執(zhí)行.class文件,DVM執(zhí)行.dex文件,dx工具會把.class文件轉為.dex包,再與資源文件一起打成.apk包,所以.apk包其實就是.zip,包含dex文件、資源文件、assets包等。
    ? 安卓應用默認只打一個dex包,安卓初次裝應用的時候會進行dex優(yōu)化,使用DexOpt工具,加載過程中會生成ODdex文件,執(zhí)行效率會更高;DexOpt會把一個dex包的所有方法id檢索起來存在一個鏈表結構里,鏈表長度使用short類型來保存,因此一個dex方法數(shù)不能超過65536個;另外,DexOpt使用LinearAlloc來存儲應用方法信息,緩沖區(qū)大小不同版本限制不同,因此低版本可能方法還沒達到65536,可以編譯但是無法安裝。
    ? 注意,再打完多個dex包后,應用只會加載主dex包,其他的dex包需要我們自己手動去加載,加載還是比較耗時的,不建議放到主線程,未加載之前調用其他dex包中的類會報錯。
    ②熱修復技術:
    ? ClassLoader中包含一個pathList對象,pathList里有個dexElements數(shù)組(每個dex都包裝成了一個Element),同一個dex里類不能重復,但不同dex里類可重復,因此類被加載的時候會去遍歷dex數(shù)組找類,找到第一個后就返回了,這時候我們可以把類單獨打成補丁dex包,插入到Elements最前面即可。
    ? 注意,如果兩個相互引用的類不在同一個dex包中,就會報錯,這是由于引用者被打上CLASS_ISPREVERIFIED標簽,就會進行dex校驗的結果(5.0以上ART運行時機制,odex優(yōu)化不會有此問題);因此安卓應用在第一次安裝的時候會優(yōu)化dex->odex,虛擬機啟動的時候有個啟動參數(shù)vertify如果為被打開,就會進行dvmVertifyClass校驗,如果兩個類的直接引用在同一個dex下,則校驗成功,打上CLASS_ISPREVERIFIED標簽,那我們要做的事就是阻止類被打上這個標簽(不讓同一個dex包中的類被直接引用),到時候就不會做校驗了。
    ? 空間團隊的做法是打了一個hack.dex包,里面只有一個AntilazyLoad.class類,在dx工具執(zhí)行之前,修改所有.class文件,在每個類的構造方法中打印了一行Syetem.out.println(AntilazyLoad.class),這就導致后面校驗不成功,就不會打上CLASS_ISPREVERIFIED標簽了(javassist的使用,類似反射);注意應用啟動的時候必須先加載進來hack.dex,否則后面AntilazyLoad.class類就會找不到報錯,另外,類被打上CLASS_ISPREVERIFIED實際上是為了提高性能的,因此我們強制不打,略有性能損失。
    ③熱修復實現(xiàn):
    ? 第一步,通過javassist修改每個.class文件,引入hack.jar中的類,阻止類被打上CLASS_ISPREVERIFIED標簽(注意hack.jar需要dex優(yōu)化過);
    ? 第二步,gradle中編寫task,實現(xiàn)在dx前執(zhí)行第一步的操作;
    ? 第三步,hack.jar可以放入assets包打入apk,然后在Application中onCreate()中先將hack.jar復制到項目private的dex目錄下;
    ? 第四步,執(zhí)行patch操作,通過反射拿到pathClassLoader中的主項目dexElements,再拿到插件中dexElements,合并兩個數(shù)組即可;
    ? 第五步,熱修復完成,補丁dex包與hack.jar過程一致。

  • Java 內存分配策略:
    Java 程序運行時的內存分配策略有三種,分別是靜態(tài)分配、棧式分配、堆式分配,對應的三種分配策略使用的內存空間分別是靜態(tài)存儲區(qū)(也稱方法區(qū))、棧區(qū)、堆區(qū)。
    ? 靜態(tài)存儲區(qū)(方法區(qū)):主要存放靜態(tài)數(shù)據(jù)、全局static數(shù)據(jù)和常量,這塊內存在程序編譯時就已經分配好,并且在程序整個運行期間都存在;
    ? 棧區(qū):當方法被執(zhí)行時,方法體內的局部變量(其中包括基礎數(shù)據(jù)類型、對象的引用)都在棧上創(chuàng)建,并在方法執(zhí)行結束時這些局部變量所持有的內存將會自動被釋放,因為棧內存分配運算內置于處理器的指令集中,所以效率很高,但是分配的內存容量有限;
    ? 堆區(qū):又稱動態(tài)內存分配,通常就是指在程序運行時直接new出來的內存,也就是對象的實例所在的區(qū)域,這部分內存在不使用時將會由Java垃圾回收器來負責回收。
    -棧與堆的區(qū)別:
    ? 在方法體內定義的局部變量、一些基本類型的變量和對象的引用變量都是在方法的棧內存中分配的,當在一段方法塊中定義一個變量時,Java 就會在棧中為該變量分配內存空間,當超過該變量的作用域后,該變量也就無效了,分配給它的內存空間也將被釋放掉,該內存空間可以被重新使用。
    ? 堆內存用來存放所有由new創(chuàng)建的對象(包括該對象其中的所有成員變量)和數(shù)組,在堆中分配的內存,將由Java垃圾回收器來自動管理,在堆中產生了一個數(shù)組或者對象后,還可以在棧中定義一個特殊的變量,這個變量的取值等于數(shù)組或者對象在堆內存中的首地址,這個特殊的變量就是我們上面說的引用變量,我們可以通過這個引用變量來訪問堆中的對象或者數(shù)組。

  • 內存泄漏相關:
    ? 定義:當一個對象已經不需要再使用了,本該被回收時,而有另外一個生命周期較長且正在使用的對象持有它的引用從而導致它不能被回收,這導致本該被回收的對象不能被回收而停留在堆內存中,這就產生了內存泄漏。
    ? 常見場景:
    非靜態(tài)內部類的靜態(tài)實例化(會持有外部類引用,靜態(tài)實例化后不會銷毀,外部引用就不會銷毀)、單例持有Context(類似上條,單例常駐內存,如果持有Activity,導致Activity不會被回收)、非靜態(tài)Handler做耗時操作(類似上條,持有外部Activity的引用,耗時操作導致外部Activity不會被回收)、io等資源使用后沒有關閉(用到緩沖,存在于虛擬機內和外,需要手動close再置null)、Bitmap沒有標記回收(使用完畢需要標記一下可回收,否則Bitmap一般是比較占內存的)、事件總線庫的注冊與解注冊(注冊后被觀察者會持有觀察者的引用,如果是Activity不解注冊會內存泄漏)。
    ? 總結:確保對象能夠在正確的時機被回收掉,在需要的時候使用,不需要的時候及時可以被回收,因此注意生命周期長的對象不要一直持有生命周期短且應該被回收的對象。
    ? 補充關于Handler內存泄漏的解釋:Handler非靜態(tài)內部類會持有外部類引用this,所以可以直接使用Activity中的屬性、方法等,因此如果有耗時操作會導致外部Activity泄漏??梢允褂渺o態(tài)內部類,杜絕直接對外類的引用,但是無法訪問外部類實例域和方法了,只能訪問靜態(tài)域,如果必須要使用外部類的實例域,則可以使用弱引用從構造方法中傳入外部類即可。

  • Java注解:
    ? 元注解:
    @Retention:注解保留的生命周期,有SOURCE/CLASS/RUNTIME三種;
    @Target:注解對象的作用范圍,有TYPE/FIELD/METHOD/PARAMETER/CONSTRUCTOR/PACKAGE等;
    @Inherited:所標注的注解在作用的類上是否可被繼承,使用時,子類會繼承該注解,但只作用于類,對方法、屬性無效;
    @Documented:javadoc的工具化文檔。
    ? 作用:降低項目耦合度、自動完成一些規(guī)律性代碼、自動生成一些Java代碼。
    ? 注意:注解使用反射,可以拿到屬性的注解對象和注解值來進行一些額外操作,因此運行時注解其實使用反射是比較影響效率的,可以使用編譯時注解在編譯時通過反射獲取注解,自動生成一些代碼,如ButterKinfe等庫的實現(xiàn),編譯時注解依賴于注解處理器AbstractProcessor,是javac的一款工具,用來編譯時掃描和處理注解,并生成Java文件注入到源碼中。

  • 編碼相關:
    編碼:ASCII最早表示英文、Unicode是中文字符集可表示更多字符但是會混淆和浪費空間(比較常見)、UTF-8/UTF-16是可變長編碼方式;因此英文使用ASCII不會有問題,中文不能使用ASCII,中英都可以使用Unicode或UTF-8。

  • 加密相關:
    ? Base64:一般網絡上傳輸?shù)臄?shù)據(jù)要進行Base64編碼一次,它不是加密算法,是一種編碼轉換,因為ASCII碼的128~255之間是不可見字符,網上傳輸數(shù)據(jù)時可能會解析錯誤,因此Base64的實際作用就是將二進制轉為可見的字符形式的數(shù)據(jù)形式。
    ? MD5/SHA1:MD5和SHA1都是一套摘要算法,不能算是加密算法,它可以把任何一個文件、程序、圖像等類型,不管體積有多大,轉化成一個固定長度的MD5或哈希值,不可逆,因此可以用來判斷文件、數(shù)據(jù)是否被更改,SHA1略叼一點,但是MD5略快一點。
    ? DES/3DES/AES:對稱加密算法,DES->3DES->AES是越來越叼,使用向量可以增強加密強度,一般用于與RSA合作雙向加密。
    ? RSA:非對稱加密算法,公鑰加密、私鑰解密,或者私鑰加密、公鑰解密,甲方發(fā)起請求,使用乙方的公鑰加密,乙方使用自己的私鑰解密;簽名認證:對發(fā)送的數(shù)據(jù)摘要出MD5值,用私鑰加密與內容一起發(fā)送,接收方使用公鑰解密得到MD5值,摘要出內容對比MD5是否一致即可。
    -項目加密過程:
    ? 登錄模式下,用戶登錄時獲取隨機AES_KEY,然后用AES加密請求數(shù)據(jù),用RSA加密AES_KEY,一起發(fā)送給服務端,服務端保存用戶AES_KEY,返回數(shù)據(jù)僅用AES加密,本地用AES解密,服務端同時會返回token,下次請求直接帶token過去,服務端就找到相應的AES_KEY進行解密。
    ? 游客模式下,用戶每次都隨機生成AES_KEY(實際上也就進入APP生成一個),然后用AES加密請求數(shù)據(jù),用RSA加密AES_KEY,一起發(fā)送給服務端,返回數(shù)據(jù)僅用AES加密,本地用AES解密。

  • 絕對路徑和相對路徑:
    ? 絕對路徑形如:E:\book\代碼\test.java,不同主機磁盤文件路徑不同,一般不使用絕對路徑;相對路徑形如:gg.png,以"/"分隔,一般使用相對路徑,通用。
    -相對路徑三種格式:
    ? ../ 當前路徑的上一級路徑;
    ? ./ 當前路徑(可以省略不寫);
    ? / 虛擬目錄的根路徑;

  • 正則表達式:
    ? \d:匹配一個數(shù)字; \w:匹配一個字母或數(shù)字;\s:匹配至少一個空格。
    ? .:匹配任意1個字符;?:匹配0個或1個字符;*:匹配0個或多個字符;+:匹配1個或多個字符。
    ? {n}:表示n個字符,用{n,m}表示n-m個字符;如\d{3}表示匹配3個數(shù)字,\d{3,8}表示3-8個數(shù)字。
    ? []:表示一個字符范圍,如[0-9a-zA-Z\_][a-zA-Z\_\$][0-9a-zA-Z\_\$]*。
    ? A|B:匹配A或B;^:表示行的開頭,如^\d表示必須以數(shù)字開頭;$:表示行的結束,如\d$表示必須以數(shù)字結束。

  • Labmda表達式:
    ? 定義:lambda是匿名函數(shù)的別名,用于簡化匿名內部類的調用。
    ? 語法:lambda表達式展示了一個接口抽象方法最有價值的兩點,即參數(shù)列表和具體實現(xiàn),(參數(shù)列表...)->{語句塊}。
    ? lambda表達式共有三種形式:函數(shù)式接口、方法引用和構造器引用。
    ? 參數(shù)只有一個時不用發(fā)加括號,沒有或大于一個需要括號;當表達式只有一句話大括號和return都可以不用加,有多行的話需要大括號,也就需要return。
    ? 方法引用的三種格式:object::instanceMethod / ClassName::staticMethod / ClassName::instanceMethod,第三種比較少見,參數(shù)列表的第一個參數(shù)為方法的調用者,其余參數(shù)為方法參數(shù);構造器引用:ClassName::New。
    ? Java8中新增了一個effective final功能,如int effectiveFinalInt = 666,不用加final修飾符可以在接口回調里使用該變量,前提是確認它是不會被修改的。
    ? 匿名內部類中,this關鍵字指匿名內部類本身的對象;而在lambda表達式中,this指向外圍的類對象。
    ? lambda表達式經過編譯器編譯后,每個表達式會增加1~2個方法數(shù),Android方法數(shù)有65536限制,需注意。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容