Android開發中優化分析及總結筆記

一、奔潰的原因及優化

       1、Android的奔潰分爲Java奔潰和Native奔潰。

            Java奔潰就是在Java代碼中,出現了未捕獲異常,導致程序異常退出。Native奔潰是因爲Native代碼中訪問非法地址,也可能是地址對齊出現了問題,或者發生了程序主動abort,這些都會產生響應的signal信號,導致程序異常退出。

       2、Native奔潰的捕獲流程:

            編譯端,編譯C/C++代碼時,需要將帶符號信息的文件保留下來;客戶端,捕獲到奔潰時,將收集到儘可能多的有用信息寫入日誌文件,然後選擇合適的時機上傳服務器;服務端,讀取客戶端上報的日誌文件,尋找適合的符號文件,生成可讀的C/C++調用棧。Breakpad是一個跨平臺的開源項目,可以集成用來捕獲Native奔潰。

       3、選擇奔潰服務:

            奔潰的服務系統有騰訊的Bugly、阿里巴巴的啄木鳥平臺等。

二、內存優化:

   內存分配:

       靜態:內存在程序編譯的時候已經分配好了,這塊內存在程序運行期間一直存在;主要放靜態數據,全局static數據和一些常亮。

       棧:在執行函數或方法時,函數內部變量存儲都放在棧中,函數存儲單元自動釋放;棧運行速度快,但數量有限。

       堆:也叫動態內存分配,是通過對象new出來的對象實例。

       區別:堆是不連續的區域,空間大;棧是一塊連續的內存區域,大小由操作系統決定,隊列的實現方式,先進後出。

       使用:成員變量全部都存在堆中(包括基本類型、引用及引用的對象實體)——類的對象最終是被new出來的;局部變量數據類型和引用存儲在棧中,引用的實體對象在堆中——它們屬於方法的變量。

  1、內存泄露說明:

        內存泄露簡單說就是對象由於編碼錯誤或者系統原因,仍然存在着對其直接或間接的引用,導致系統無法進行回收。內存泄露容易留下邏輯隱患,並增加了應用內存峯值與發生OOM的概率。

 2、造成的常見原因:

     靜態對象、this$0、系統、監聽器、線程、Textline、廣播、定時器、輸入法、Web View、Handler、音頻等對象被持有導致無法釋放或不能按照對象正常的生命週期進行釋放。

3、內存泄露的監控方案:

   Square的開源庫leakcanry通過弱引用方式偵查Activity或對象的生命週期,若發現內存泄露自動dump Hprof文件,最終能展現出來內存泄露發生的具體位置。

4、兜底回收內存:

   Activity泄露會導致改Activity應用到的Bitmap、DrawingCache等無法釋放,對內存造成大的壓力,兜底回收是指已泄露Acivity,嘗試回收持有的資源,泄露的僅僅是一個Activity空殼,從而降低對內存的壓力。做法是在Activity onDestory時候從view的rootview開始,遞歸釋放所有子view涉及的圖片,背景,DrawingCache,監聽器等等資源,讓Acivity成爲一個不佔資源的空殼,泄露了也不會導致圖片資源被持有。

   …
   …
   Drawable d = iv.getDrawable();
   if (d != null) {
       d.setCallback(null);
   }        
   iv.setImageDrawable(null);
   ...
   ...

降低運行時內存的一些方法:

 1、減少bitmap佔用的內存:

     1)、防止bitmap佔用資源多大導致OOM

     2)、圖片按需加載:圖片的大小不應超過view的大小。在把圖片載入內存之前,先計算出一個合適的inSampleSize縮放比例,避免不必要的大圖載入。對此,可重載drawable與ImageView,例如在Acivity ondestroy時,檢測圖片大小與View的代銷,若超過,可以上報或提示。

    3)、統一的bitmap加載器:Picasso、Fresco、ImageLoader加載庫有了統一的bitmap加載器,我們可以在加載bitmap時,若發生OOM(try catch方式),可以通過清除cache,降低bitmap format(ARGB8888/RBG565/ARGB4444/ALPHA8)等方式,重新嘗試。

   4)、圖片存在像素浪費:對於.9圖,美工可能在出圖時在拉伸與非拉伸區域都有大量的像素重複。通過獲取圖片的像素ARGB值,計算連續相同的像素區域,自定義算法判定這些區域是否可以縮放。關鍵也是需要將這些工作做到系統化,可及時發現問題,解決問題。

  2、使用多進程:

     對於webview,圖庫等;由於存在內存系統泄露或者佔用內存過多的問題,我們可以採用單獨的進程。

  3、上報OOM詳細信息:當系統發生OOM的crash時,我們應當上傳更加詳細的內存相關信息,方便我們定位當時內存的具體情況。其他例如使用large heap、inBitmap、SparseArray、Protobuf等不再一一細述,對代碼採用優化--埋坑--優化--埋坑的方式並不推薦。我們應該着力於建立一套合理的框架與監控體系,能及時的發現諸如bitmap過大、像素浪費、內存佔用過大、應用OOM等問題。

 

三、卡頓優化:

   卡頓排查工具:

       traceview性能分析工具,它利用Android Runtime函數調用的event事件,講函數運行的耗時和調用關係寫入trace中。

       systrace是Android4.1新增的心梗分析工具。通常使用systrace跟蹤系統的I/O操作、CPU負載、Surface渲染、GC等事件。

   卡頓處理:

       過於複雜的佈局:界面性能取決於UI渲染性能,CPU負責UI佈局元素的Measure,Layout、Draw等相關運算執行。可以藉助Hierarchy Viewer工具幫助我們分析佈局,Hierarchy Viewer可以以圖形化樹狀結構形式展示出UI層級,還對每個節點給出了三個小圓點,以指示改元素Measure,Layout、Draw的耗時及性能。

       過度繪製(Overdraw):用來描述一個像素在屏幕上多少次被重繪在一幀上。Android系統提供了可視化的方案讓我們很方便查看overdraw的現象:在“系統設置”——>“開發者選項”——>“調試GPU過度繪製”中開啓調試。

       UI線程的複雜運算:UI線程的複雜運算會造成UI無響應,使用traceview工具分析。

       頻繁的GC:在執行GC操作的時候,任何線程的操作都需要暫停,等待GC操作完成之後,其他操作才能繼續運行,故而頻繁GC會導致界面卡頓。頻繁GC的原因:一、內存抖動:大量的對象被創建又在短時間內馬上被釋放;瞬間產生大量的對象會嚴重佔據內存區域,從而觸發更多的GC。解決方案:一般瞬間大量產生對象是因爲在代碼循環中new對象,或是在Ondraw方法中創建對象等;儘量不要在循環中大量的使用局部變量。

 

四、啓動優化:

        1、啓動的過程分析:點擊響應應用解釋——>預覽窗口顯示——>Application創建——>閃屏Activity創建界面準備——>閃屏顯示——>主頁顯示——>其他工作——>窗口可操作。

        2、啓動問題分析:

            問題1、點擊圖標很久都不響應;問題2、首頁顯示過慢;問題3、首頁顯示後無法操作。

        3、啓動過程避免進行大量的字符串操作,特別是序列化跟反序列化過程,一些頻繁創建的對象,例如網絡庫和圖片庫的Byte數組、Buffer可以複用。如一些模塊實在需要頻繁創建對象,可以考慮移到Native實現。

    

五、I/O優化:

         1、Linux I/O的概念:文件I/O操作由應用程序、文件系統和磁盤共同完成。首先應用程序將I/O命令發送給文件系統,然後文件系統會在合適的時機把I/O操作發給磁盤。

            文件系統I/O:應用程序調用read()方法,系統會通過中斷從用戶空間進入內核處理,然後經過VFS(Virtual File System,虛擬文件系統)、具體文件系統、頁緩存Page Cache 。

            磁盤:是指系統的存儲設備,應用程序要read()的數據沒有在頁緩存中,這時候就需要真正向磁盤發起I/O請求。這份過程要先經過內核的通用塊層、I/O調度層、設備驅動層,最後纔會交給具體的硬件設備處理。

        2、Android I/O:手機使用的存儲設備,用閃存作爲存儲設備,也就是我們常說的ROM。

        3、I/O的三種方式

             標準I/O:應用程序平時用到read/write操作都屬於標準I/O,也就是緩存I/O(Buffered I/O);特性是對於讀操作——當應用程序讀取某塊數據時,如存在頁緩存中則立即返回給應用程序,不需實際的物理讀盤操作;對於寫操作——數據先寫到頁緩存中,數據是否立即寫到磁盤取決於寫操作的機制。 

             直接I/O:訪問文件方式減少了一次數據拷貝和一些系統調用的耗時,很大程度降低了CPU的使用率及內存的佔用。

             mmap:Android系統啓動加載DEX時,不會把整個文件一次性讀到內存中,而是採用mmap的方式。通過把文件映射到進程的地址空間、最終映射的物理內存依然在頁緩存中;帶來的好處,減少系統調用,一次mmap()系統調用後所有的調用會像操作內存一樣不會出現戴亮的read/write系統調用;減少數據拷貝,mmap只需從磁盤拷貝一次就可以;可靠性高。

         4、多線程阻塞I/O和非阻塞NIO

               多線程阻塞I/O,讀寫收到I/O性能瓶頸的影響,在到達一定速度後整體性能就會收到明顯的影響,過多的線程反而會導致應用整體性能的明顯下降。實際開發中大部分都是讀一些比較小的文件,使用單獨的I/O線程還是專門新開一個線程,其實差別不大。 

               非阻塞的NIO是以事件的方式通知,的確可以減少線程切換的開銷。Chrome網絡庫是一個使用NIO提升性能很好的例子,特別在系統非常繁忙時。但是NIO的缺點也非常明顯,應用程序的實現會變得更復雜,有的時候異步改造並不容易。使用NIO的最大作用不是減少讀取文件的耗時,而是最大化提升應用整體的CPU利用率。

         5、監控線上的I/O操作:分爲有Java Hook和Native Hook

            Hook的四個接口可以採集到的信息:open——文件名、fd、文件原始大小、堆棧、線程;read、write——類型、讀寫次數、讀寫總大小、使用buffer大小、讀寫總耗時;close——打開問價總耗時、最大的連續讀寫時間。

 

六、存儲優化:

       1、Android的存儲基礎:

            Android的分區;分區一般來說就是講設備的存儲劃分爲一些互不重疊的部分,每個部分都可以單獨格式化,用作不同的目的。這樣系統就可以靈活的針對單獨分區做到不同的操作。

           /system分區:存放所有Google提供的Android組件的地方,以只讀方式mount。

           /data分區:存放用戶數據的地方。

           /vendor分區:存放廠商特殊系統修改的地方。

           /cache分區:系統升級過程使用的分區或recovery。

          /storge分區:外置或內置sdcard。

      2、Android存儲安全:

          權限控制:Android的每一個應用都在自己的應用沙盒內運行,沙盒使用了標準Linux的保護機制,通過爲每個應用創建獨一無二的Linux UID來定義。

          數據加密:Android的有兩種設備加密方式——全盤加密和文件級加密。

     3、常見的數據存儲方法:

          存儲就是把特定的數據結構轉化成可以被記錄和還原的格式,這個數據格式可以是二進制、XML、JSON、protocol Buffer等。

         SharedPreferences存儲方式:

              跨進程不安全——由於沒有使用跨進程的鎖,SharedPreferences在跨進程頻繁讀寫有可能導致數據全部丟失。

              加載緩慢——SharedPreferences文件的加載使用異步線程,加載線程並沒有甚至線程優先級,如主線程讀取數據就需要文件加載線程的結束。

              全量寫入——無論是調用commit()還是apply(),即使值改動其中的一個條目,都會把整個內容全部寫入到文件。

              卡頓——由於提供了異步落盤的apply機制,在奔潰或其他異常情況可能會導致數據丟失。

            系統提供SharedPreferences的應用場景是用來存儲一些非常簡單、輕量的數據。我們不要使用它來存儲過於複雜的數據。

       ContentProvider存儲方式;

              ContentProvider的生命週期默認在Application onCreat()之前,而且都是在主線程創建的,在自定義ContentProvider類的構造函數、靜態代碼塊、onCreat函數都儘量不要做耗時的操作,會拖慢啓動速度。

              ContentProvider在進行跨進程數據傳遞時,利用了Android的Binder和匿名共享內存機制。簡單說就是通過Binder傳遞CursorWindow對象內部的匿名共享內存的文件描述符。這樣在跨進程傳輸中,結果數據並不需要跨進程傳輸,而是在不同進程中傳遞匿名內存文件描述符來操作同一塊匿名內存,這樣來實現不同進程訪問相同數據的目的。

      對象的序列化:

              應用程序中的對象存儲在內存中,如想把對象存儲下來或網絡傳輸,這個時候就需要用到對象的序列化和反序列化,對象序列化就是把一個Object對象所有信息表示成一個字節序列,這個包括Class信息、繼承關係信息、訪問權限、變量類型以及數值信息等。

              Serialzable序列化;

                  是Java原生的序列化機制,可通過Serializable將對象持久化存儲,也可通過Bundle傳遞Serializable的序列化數據。

                 Serializable的原理是通過ObjectInputStream和ObjectOutStream來實現的。從源碼上看,整個序列化過程使用了大量的反射和臨時變量,而且在序列化對象時,不僅會序列化當前對象本身,還需要遞歸序列化對象應用的其他對象。

                WriteObject和readObject方法。Serializable序列化支持替代默認流程,它會先反射判斷是否存在自己實現的序列化方法WriteObject或反序列化readObject。通過這兩個方法,我們可以對某些字段做一些特殊修改,也可以實現序列化的加密功能。

                writeReplace和readResolve方法。這兩個方法代理序列化的對象,可以實現自定義返回的序列化實例。     

                Serializable不被序列化的字段,類的static變量以及被聲明爲transient的字段,默認的序列化機制都會忽略的字段,不會進行序列化存儲;也可以使用writeReplace和readResolve方法做自定義的序列化存儲。

                 serialVersionUID,在類實現了Serializable接口後,需添加一個Serial Version ID,類似與類的版本號。

                 構造方法,Serializable的反序列默認是不會執行構造函數的,它是根據數據流中對Object的描述信息創建對象的。

              

            Parcelable序列化:

                 Parcelable的序列化只會在內存中進行操作,不會將數據存儲到磁盤裏。

                 Parcelable的寫入和讀取的時候都需要手動添加自定義代碼,使用起來相比Serializable會複雜;Parcelable不需要採用反射的方式去實現序列化和反序列化。

        

        數據的序列化;

              JSON數據:

                     JSON是一種輕量級的數據交互格式,它被廣泛使用於網絡傳輸中,很多應用的服務端的通信都是使用JSON格式進行交互。

                     JSON的優勢:相比對象序列化方案,速度更快,體積更小‘相比二進制的序列化方案,結構可讀,易於排查問題;使用方便,支持跨平臺、跨語言。支持嵌套引用。

              Protocol Buffers:

                      數據量大Protocol Buffer是一個好選擇。

     4、數據庫SQLite的使用及優化:

              SQLiteDatabaseLockedException的異常的原因是併發導致的。

              多進程併發:多進程可以同時獲取SHARED鎖來讀取數據,但是隻有一個進程可以獲取EXCLUSIVE鎖來寫數據庫。EXCLUSIVE模式下,數據庫連接在斷開前都不會釋放SQLite文件的鎖,從而避免不必要的衝突。提高數據庫訪問的速度。

              多線程併發:SQLite支持多線程併發模式,需要開啓多線程配置。系統SQLite會默認的開啓多線程Multi-thread模式,SQLite所的粒度都是數據庫文件級別,並沒有實現表級甚至級的鎖;同一個句柄一時間只有一個線程在操作,這時需要打開連接池Connection Pool。

              在寫之間是不能併發的,如出現多個並情況,依然可能會出現SQLiteDatabaseLockedException。這時可以讓應用中捕獲這個異常,然後等待一段時間再重試。

             查詢優化:

               索引優化:建立索引是有代價的,需要一直維護索引表的更新;比如對於一個很小的表來說就是沒必要建立索引;如一個表經常是執行插入更新操作,那麼也需要節制的建立索引。索引優化是SQLite優化中最簡單同時也是最有效的,但是它並不是簡單的建一個索引就可以了,有時需要進一步調整查詢語句甚至是表的結構,這樣才能達到最好的效果。

            頁大小與緩存大小:

            其他優化:通過引進ORM,可以大大的提升開發效率。通過WAL模式和連接池,可以提高SQLite的併發性能。通過正確的建立索引,可以提升SQLite的查詢速度。通過調整默認頁大小和緩存大小,可以提升SQLite的整體性能。

            SQLite的監控:

                    本地測試——SQL語句都應該先在本地測試,通過EXPLAIN QUERY PLAN測試SQL語句的查詢計劃,是全表掃描還是使用了索引,以及具體使用了哪個索引。

                    耗時監控——

                    智能監控——

             

五、網絡優化:

           網絡性能評估:延遲——數據從信心源發送到目的地所需的時間;帶寬——邏輯或物理通信路徑最大的吞吐量。

           網絡數據包的發送過程:數據包從手機出發要經過無線網絡、核心網絡以及外部網絡,才能到達我們的服務器。

           網絡優化的核心內容

                  速度:在網絡正常或者良好的時候,這樣更好地利用寬帶。進一步提升網絡請求速度;

                  弱網絡:移動端網絡複雜多變,再出現網絡連接不穩定時,怎樣最大保證網絡的連通性;

                  安全:網絡安全不容忽視。怎樣有效防止被第三方劫持、竊聽甚至篡改;

           網絡請求的整個過程

                  DNS解析:通過DNS服務器,拿到對應域名的IP地址,有DNS解析耗時情況、運營商LocalDNS的劫持、DNS調度;

                  創建連接:與服務器簡歷連接,包括TCP三次握手、TLS祕鑰協商。有多個IP端口該如何選擇、是否要使用HTTPS、能否減少省下創建連接的時間;

                  發送/接收數據:在成功建立連接之後,可以跟服務器交互,進行組裝數據、發送數據、接收數據、解析數據。關注問題是,如何根據網絡狀況將寬帶利用好、怎樣快速地偵測到網絡延時、在弱網絡下如何調整包大小;

                  關閉連接:關注主動關閉和被動關閉兩種情況,一般希望客戶端可以主動關閉連接。

           

六、UI優化:

        1、屏幕與適配:

             px:像素點;

             ppi:像素密度,沒英寸所包含的像素數目。這是屏幕物理參數;

             dpi:像素密度,在系統軟件上指定的單位尺寸的像素數量。與ppi不同的是,dpi可能會被人爲的調整;

             dp:基於屏幕物理分辨率一個抽象的單位,用月說明與密度無關的尺寸和位置;

             density:密度,屏幕上每平方英寸中含有的像素點數量。

通過dp加上自適應佈局可以基本解決屏幕的碎片化的問題,也是Android推薦使用的屏幕兼容適配方案。適配方案的博客有——《Android目前穩定高效的UI適配方案》、《smallestWidth限定符適配方案

       2、UI優化的常用手段:

            儘量使用硬件加速:硬件加速繪製的性能是遠遠高於軟件加速的,所以UI優化的第一手段就是保證渲染儘量使用硬件加速。有些情況不能使用硬件加速,是因爲硬件加速不能支持所有的Canvas API,具體API兼容列表可以見drawing-support文檔。如使用了不支持的API,系統需要通過CPU軟件模擬繪製,這也是漸變、磨砂、圓角等小狗渲染性能比較低的原因。

             Creat View優化

                  View的創建是在UI線程中,創建時會包括各種XML的隨機讀的I/O時間、解析XML的時間、生成對象那個的時間(Framework會大量使用到反射)。

                 使用代碼創建:使用XML進行編寫可以Android Studio中實時預覽界面,如果對一個界面進行極致優化,就可以使用代碼進行編寫界面。

                 異步創建:在線程提前創建View,實現UI預覽。如這樣會拋出異常。解決方法是在使用線程創建UI的時候,先把線程Looper的MessagaQueue替換成UI線程Looper的Queue。在創建萬View後需要吧線程的Looper恢復成原來的。

                 View重用:正常來說,View會隨着Activity的銷燬而同時銷燬。ListView、RecycleView通過View的緩存與重用大大地提升渲染性能。因此可以參考這些思想,實現一套可以在不同Activity或者Fragment使用的View緩存機制。

                 measure/layout優化:減少UI佈局層次,儘量扁平化,使用<ViewStub><Merge>等優化;優化layout的開銷,儘量不適用RelativeLayout或者幾月weighted LinearLayout,這兩者layout的開銷非常巨大。推薦使用ConstrainLayout替代。

                 背景優化:儘量不要重複去設置背景,主題背景(theme),theme默認會是一個純色背景,如自定義界面背景,主題背景就是無用的。主題背景是設置在DecorView中,所以會帶來重複繪製。2018發佈的PrecomputeredText集成在jetpack中,可以異步進行measure和layout,不必在主線程中執行。

              UI優化的進階手段

                   Litho異步佈局

                       是Facebook開源的聲明式Android UI渲染框架,一般來說Android所有的控件繪製都要遵守measure->layout->draw的流水線並且都發生在主線程中。和PrecomputeredTex一樣,把measure和layout都放到了後臺線程,只留下了必須要在主線程完成的draw,這樣可以大大降低UI線程的負載;

               Litho還優化了RecyclerView中UI組件的緩存和回收方法。原生的RecyclerView或ListView是按照viewType來進行緩存和回收,但RecyclerView中出現viewType過多,會使緩存形同虛設,但Litho是按照text、image和video獨立回收的,這可以提高緩存命中率、降低內存使用率、提高滾動幀率。

                 Flutter自己的佈局+渲染引擎

                      Flutter是Google推出並開源的移動應用開發框架,開發着可以通多Dart語言開發APP,一套代碼可以同時在IOS和Android平臺。 在Android上Flutter完全沒有基於系統的渲染引擎,而是吧Skia引擎直接集成進了APP中,這使得Flutter APP就像一個遊戲App。並且直接使用了Dart虛擬機,可以說是一套跳脫出Android的方案,所以Flutter也可以很容易實現跨平臺。 

                 RenderThread與RenderScript

                       在Android5.0,系統增加了RenderThread,對於ViewPropertyAnimator和CircularReveal動畫,我們可以使用RenderThread實現動畫的異步渲染。但主線程阻塞的時候,普通動畫會出現明顯的丟幀卡頓,而是使用RenderThread渲染的動畫即使阻塞了主線程仍然不受影響。

                      圖片的變換涉及大量的計算任務,可以通過RenderScript,他是Android操作系統上的一套API。基於異構計算思想,專門用於密集計算。

                 

七、包體積的優化

          安裝包的文件結構

             res/:存放編譯後的資源文件,如Drawable、Layout等;

             assets/:應用程序的資源,應用程序可以使用AssetManager來檢索該資源;

             META-INF/:該文件夾一般存放於已經簽名的apk中,包含了apk中所有文件的簽名摘要等信息;

             resources.arsc:編譯後的二進制資源文件;

             AndroidManifest.xml:Android的清淡文件,用於面熟應用程序員的名稱、版本、所需權限、註冊的四大組件。

  

            

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章