Android開發隨記

1、性能優化

性能優化能夠在這幾個方面下手, 流暢性穩定性包體積大小
  1. 流暢性優化
    1. 啓動時間優化—在Application的onCreate的時候,會有不少SDK選擇在這裏進行初始化,在加上本身寫的一些庫也在這裏初始化,這樣主線程在初始化的時候將會不堪重負,致使啓動好久白屏,因此在初始化的時候應當進行
      1. 根據庫進行分步延遲加載
      2. 多線程加載
      3. 後臺任務加載
    2. UI優化—UI層級過深,在進行測量和定位的時候將會佔用更多的CPU資源,也會致使渲染週期加長,在Android的渲染機制中,每16ms將會發起一次垂直同步信號,進行渲染,若是在16ms之內還沒法更新到surface,畫面將會顯示上一次的畫面,這樣看起來就會卡頓。解決措施:
      1. 減小布局層級
      2. 使用懶加載標籤ViewStub
      3. 避免使用include,改爲使用merge標籤
      4. 儘可能避免使用複雜的矢量動畫和矢量圖形,繪製矢量圖形的須要佔用cpu資源,也會致使卡頓,複雜的矢量圖形可使用位圖,GPU會進行緩存。
    3. 避免大量的IO — 大文件IO是很是佔用CPU的耗時操做,必要時能夠進行分佈,分片的IO操做,對於不須要操做數據庫的數據應當使用文件保存,小文件讀寫比數據庫更快,也避免數據庫冗餘。
    4. 避免頻繁GC — 避免頻繁大量的建立對象,當內存緊張時,會頻繁GC,申請大內存的對象,也會有可能觸發GC,GC時會佔用CPU,致使畫面卡頓
    5. 合理的使用線程 — 線程的切換是又開銷的,頻繁的切換線程是會使用性能下降,應當建立cpu核數至關的線程池,合理分配線程,和使用協程
    6. 避免過多的複雜計算 做爲前端也不該該進行復雜的運算,又不是超算,密集的複雜計算也會佔用更多的cpu資源。
  2. 穩定性優化
    影響App穩定性常見的有兩個場景 Crash 和 ANR,它會致使App異常退出。因此解決App的穩定性應該列爲最高優先級。如何避免異常的發生,能夠從這幾個方面入手
    1. 編碼階段。人非機器,即便是機器也會出錯,因此應該使用更多的工具輔助,在編碼的時候儘可能把異常狀況排除掉。
      1. 空指針異常。最多見的異常就是空指針異常的,我建議使用kotlin,有空安全類型。
      2. 內存泄漏,發生內存泄漏的主要緣由的生命週期不一致的對象相互引用,好比在線程中,handler,靜態單例裏引用了Activity,Activity銷燬後,沒有被釋放。要解決的這個除了改變編程習慣,也可使用一些協同Activity的生命週期的工具類來使用線程和handler,在Activity銷燬的時候把Activity的引用釋放,避免不規範的建立線程,handler,致使內存泄漏。
      3. OOM。在App中常見的是加載大圖等內存的大戶。因此圖片要進行壓縮,讀取的時候不要直接將大圖載入內存中,先獲取圖片信息,在設置壓縮比例inSampleSize在加載,最好使用Glide,Picasso這些優秀的開源庫加載,他們有對圖片緩存管理。
      4. 至於其餘的bug,若是時間容許能夠編寫單元測試,也可使用相似Android Lint,Findbugs的工具排查。多人團隊開發的,能夠互相審查代碼,一來能夠看出本身沒有察覺的bug,二來也能熟悉他人的代碼。
    2. Carsh信息監控上報。這個不少第三方平臺都有,app的必需品,這裏就不打廣告了。若是要本身寫的話,Java層,除了設置UncaughtExceptionHandler以外,還須要獲取AMS.getProcessesInErrorState,native層的話須要設置sigaction 和使用libunwind這個庫了。
  3. 包體積的大小的優化
    1. 只使用一套高分辨率的資源圖,使用工具對圖片進行壓縮,圖片使用webp格式。
    2. 對於so文件只使用v7a平臺的。固然這是犧牲性能爲代價的處理方式。下列內容轉自:https://www.cnblogs.com/yingsong/p/6709322.html
      1. armeabiv-v7a: 第7代及以上的 ARM 處理器。2011年15月之後的生產的大部分Android設備都使用它.
      2. arm64-v8a: 第8代、64位ARM處理器,不多設備,三星 Galaxy S6是其中之一。
      3. armeabi: 第5代、第6代的ARM處理器,早期的手機用的比較多。
      4. x86: 平板、模擬器用得比較多。
      5. x86_64: 64位的平板。
    3. 使用7z打包。能夠參考微信的AndResGuard

2、內存模型

Linux的進程內存模型是由用戶空間內核空間組成。html

  • 內核空間。在這裏CPU能夠訪問任何外圍設備,好比什麼鍵盤,顯示器,網卡,固然這些在CPU的眼裏都是一段物理地址。換句話說,在內核空間CPU能夠訪問全部的物理地址。這個內核空間是全部進程共享的。
  • 用戶空間。在這裏CPU的訪問是受限的,好比操做系統給它分配了2G的空間,它也就只能訪問這2G的地址了。這個是進程獨享的,其餘的進程沒法訪問這個空間。

在應用程序中,若是直接操做外圍設備,訪問時也不知道其餘程序有沒有在訪問,也不知道哪一段能夠用的,你們你爭我搶的,都亂套了,並且也不安全。因此須要一位管理者--操做系統。操做系統將真實的物理地址隔離起來,給每一個程序分配一段虛擬地址,經過mmap將真實地址和虛擬地址起來,好比虛擬地址是0x00,那麼它真實的物理地址多是0x1c。在真實物理地址它可能不是一段連續的地址,可是在虛擬地址是連續的就能夠了。前端

虛擬空間還能夠進行細分:java

內核空間(進程管理,存儲管理,文件管理,設備管理,網絡系統等)
----------
棧
FileMapping
堆
BSS
Data
text
複製代碼

  • 內核空間。這裏主要是一些進程管理,存儲管理,文件管理,設備管理,網絡系統等。因爲這部分是全部進程共享的,爲了更高效率的通訊,在Android中設計了一塊匿名共享內存,只要將數據從用戶空間拷貝到這裏其餘進程就能夠獲取,這樣就能夠實現高效率的進程間通訊。具體能夠看看微信的MMKV的原理,Binder也是這個原理。
  • 用戶空間
    1. 。這一塊不是很大,主要保存一些方法的地址,局部變量表,返回地址等。因此遞歸很容易就StackOverFlow。
    2. 文件地址映射塊。這裏記錄了虛擬地址對實際文件物理地址的映射,包括動態連接庫文件。內存文件映射的物理存儲器來自一個已經存在於磁盤上的文件,並且在對該文件進行操做以前必須首先對文件進行映射。使用內存映射文件處理存儲於磁盤上的文件時,將沒必要再對文件執行I/O操做,使得內存映射文件在處理大數據量的文件時能起到至關重要的做用。
    3. 。這個區間是咱們要重點關注的,由於它徹底由咱們程序員來控制。native申請的空間爲native heap,Java申請的空間則爲dalvik heap。在Android系統中,有對Java進程申請堆內存空間進行限制,這個閾值在不一樣手機上不一樣,好比48MB。超過了這個值就會發生OOM。若是想要突破這個限制,有兩個方法
      • 申請大內存。android:largeHeap=」true」
      • 建立子進程。android:process
    4. BSS這個區間保存的是一些沒有初始化的全局變量,好比 int a;沒有映射實際的物理地址,只是記錄一下所須要用到的內存空間,因此這樣寫的變量是不會有默認的賦值。
    5. Data。這個區間保存的是已經初始化的全局變量。好比int a=123。
    6. 代碼。保存程序文本。這個區域是隻讀的,防止被修改。

3、JVM 內存模型

進程由n個線程組成,在JVM中,又對進程以線程爲單位對內存進行劃分。android


線程的內存分配:
  •  棧[私有] : 
    • Java虛擬機棧
      • 棧幀
        1. 局部變量表
        2. 操做數棧
        3. 動態連接
        4. 方法返回地址
        5. 附加信息
    • 本地方法棧
    • 程序計數器
  •  堆[共享]: 
    • Java堆: 
      • 新生代 
      • 老年代 
    • 方法區: 
      • class信息:
        1. 類和接口的全限定名 
        2. 屬性名稱和描述符 
        3. 方法名稱和描述符
        4. 運行時常量池
      • 編譯後的代碼

在操做系統看來,JVM是一個程序,而Java程序只是運行在程序上的程序,因此JVM須要模擬程序運行的環境。程序員


(圖片來源:csdn-驍兵
web

  • Java虛擬機棧。Java棧由不少個棧幀組成,每個棧幀表明一個方法,而棧幀由局部變量表,操做數棧,動態連接,返回值地址以及一些附加信息組成,棧是方法的生存之地,當方法被調用的時候:
          1. 將調用方的地址入棧,也就是方法返回地址
          2. 給方法開闢棧幀,具體這個棧幀的須要多大的空間,在class文件就能夠獲得。
          3. 初始化棧幀空間。
          4. 將參數壓入局部變量表。
          5. 將參數和局部變量壓入局部變量表。
          6. 操做棧和程序計數器工做。
          7. 執行到方法返回指令,回到調用點。
      • 局部變量表。方法的執行其實就是值的存取,運算。因此方法須要以棧爲基,在局部變量表中,以slot爲單位,一個蘿蔔一個坑,用來存放int,short,float,boolean,char,byte,引用地址和返回值地址等。long 和 double 這兩個不同,一個蘿蔔兩個坑,由於他們是64位的,前面的是32位的。若是時基本數據類型,值保存在棧中,其餘引用類型存在堆中,引用地址則保存在棧中,好比int[]。至於初始化局部變量表時須要多少坑位,在方法編譯成class以後就定下來了。爲了節省空間,坑位也會複用,好比a變量出了做用域,後面定義的b變量就會複用。

        public class Test {
        
            public void test(int b, int a) {
        
                int x = 6;
                if (b > 0) {
                    String str = "VeCharm";
                }
                int y = a;
                int c = y + b;
            }
        }
        ----------------
        javac Test.java
        javap -v Test
        ----------------
        class信息:
          Last modified 2019-3-31; size 347 bytes
          MD5 checksum b0e2fc2ec7a2d576136a693c77213446
          Compiled from "Test.java"
        public class com.vecharm.lychee.sample.api.Test
          minor version: 0
          major version: 52
          flags: ACC_PUBLIC, ACC_SUPER
        Constant pool:
          ...
        {
          public com.vecharm.lychee.sample.api.Test();
            descriptor: ()V
            flags: ACC_PUBLIC
            Code:
              stack=1, locals=1, args_size=1
                 0: aload_0
                 1: invokespecial #1                  // Method java/lang/Object."<init>":()V
                 4: return
              LineNumberTable:
                line 3: 0
        
          public void test(int, int);
            descriptor: (II)V
            flags: ACC_PUBLIC
            Code:
              stack=2, locals=6, args_size=3
                 0: bipush        6
                 2: istore_3
                 3: iload_1
                 4: ifle          11
                 7: ldc           #2                  // String VeCharm
                 9: astore        4
                11: iload_2
                12: istore        4
                14: iload         4
                16: iload_1
                17: iadd
                18: istore        5
                20: return
              LineNumberTable:
                line 7: 0
                line 8: 3
                line 9: 7
                line 11: 11
                line 12: 14
                line 13: 20
              StackMapTable: number_of_entries = 1
                frame_type = 252 /* append */
                  offset_delta = 11
                  locals = [ int ]
        }
        SourceFile: "Test.java"    
        複製代碼

      • 看test方法,咱們來逐步分析這些JVM指令
          1. bipush 6。將 6 push操做棧,當int取值-1~5採用iconst指令,取值-128~127採用bipush指令,取值-32768~32767採用sipush指令,取值-2147483648~2147483647採用 ldc 指令。
          2. istore_3。將6這個值從操做棧彈出,存入局部變量表3號坑,爲啥是3號坑而不是1和2,由於這兩個坑被參數b,和參數a棧了。
          3. iload_1。將局部變量表中的1號坑的值push操做棧,1號坑的是b的值。
          4. ifle 11。將操做棧彈出b的值,ifle這條指令的意思當棧頂int型數值小於等於0時跳轉,跳轉到11偏移地址。
          5. ldc #2。將int、float或String型常量值從常量池中推送至操做棧棧頂。
          6. astore 4。將操做棧棧頂的值彈出存入局部變量表4號坑,istore就是存int值和布爾值,fstore就是存float值,astore是存引用地址的。
          7. iload_2。取出2號坑的值push操做棧。
          8. istore 4。將操做棧頂的值存入4號坑,4號坑以前str已經用過了,可是出了做用域已經無用,因此能夠複用。
          9. iload 4。取出4號坑的值push操做棧。
          10. iload_1。將局部變量表中的1號坑的值push操做棧,如今操做棧有兩個值了,
          11. iadd。將操做棧的值相加。
          12. istore 5。將結果存入5號坑。
      • 看到這想必已經明白局部變量表的做用了。
      • 操做棧。JVM須要模擬cpu那樣執行指令,但並沒有法像cpu那樣方便調用寄存器保存臨時值。因此想了一個法子,在棧中劃一塊區域做爲相似寄存器那樣的功能。
      • 動態連接。Java做爲一門多態的語言,確定少不了繼承。有一Son類繼承了Father類,重寫了say()方法。當方法執行的時候,這個方法是屬於Son這個版本仍是Father這個版本呢。因此就不能寫死方法是誰的,而是搞一個符號,等到運行時才替換成真正的版本,這被稱爲動態分配。但也有某些方法簽名是肯定永不變的,好比靜態方法,私有方法等這些不可重寫的方法的稱爲非虛方法,它們的分配稱爲靜態分配,反之可重寫的爲虛方法。因爲方法使用頻繁,因此每個類配備一個虛方法表方便索引。在Java虛擬機提供了幾條方法執行的指令。
        • invokestatic:調用static方法。
        • invokespecial:只能調用三類方法:<init>方法;final方法;private方法;super.method()。由於這三類方法的調用對象在編譯時就能夠肯定。
        • invokevirtual:調用虛方法。
        • invokeInterface:調用接口方法,會在運行時再肯定一個實現此接口的對象。
        • invokeDynamic:執行動態方法,它容許應用級別的代碼來肯定執行哪個方法調用,先在運行時動態解析出調用點限定符所引用的方法,而後再執行該方法。
        • 方法信息保存在方法區的類信息裏面。
      • 方法返回地址。調用點的地址。
  • 本地方法棧。執行native方法的棧。虛擬機能夠自由實現它,在HotSopt虛擬機把本地方法棧和Java棧融合在一塊兒。
  • 程序計數器。做爲一個JVM虛擬機,它執行class字節碼指令,須要記錄代碼執行到哪一條指令,換句話說也就是行號。JVM有200多條指令,最多不超過0xff條。若是感興趣能夠訪問這個blog.csdn.net/lm2302293/a…
  • 堆。Java堆用來存儲數據,類實例對象,全部線程共享。雖然不用關係釋放,由垃圾處理器處理,但處理不慎仍是會有內存泄漏的問題。
  • 方法區。Java中很是重要的一個區域,因此它和堆同樣,是被線程共享的,常量嘛,確定是共享的了。在方法區中,存儲了每一個類的信息。在每一個類中存放:
      • 運行時常量池
        • 字面量
        • 字段符號引用/直接引用
        • 方法符號引用/直接引用
        • 屬性
      • 字段數據。存放名稱,類型,修飾符,屬性。
      • 方法數據。存放名稱,返回類型,參數類型,修飾符,屬性。
      • 方法代碼。
        • 簽名和標誌位
        • 字節碼
        • 操做棧大小,本地變量表大小,本地變量表
        • 行號
        • 異常表。
          • 開始點
          • 終結點
          • 異常處理代碼的位置
          • 異常類在常量池的索引
      • Classloader。
  • public class Test {
    
        public void test(int b, int a) {
    
            int x = 6;
            if (b > 0) {
                String str = "VeCharm";
            }
            int y = a;
            int c = y + b;
        }
    }
    ----------------
    javac Test.java
    javap -v Test
    ----------------
    class信息:
    ...
    Constant pool:
       #1 = Methodref #4.#14 // java/lang/Object."<init>":()V
       #2 = String #15 // VeCharm
       #3 = Class #16 // com/vecharm/lychee/sample/api/Test
       #4 = Class #17 // java/lang/Object
       #5 = Utf8 <init>
       #6 = Utf8 ()V
       #7 = Utf8 Code
       #8 = Utf8 LineNumberTable
       #9 = Utf8 test
      #10 = Utf8 (II)V
      #11 = Utf8 StackMapTable
      #12 = Utf8 SourceFile
      #13 = Utf8 Test.java
      #14 = NameAndType #5:#6 // "<init>":()V
      #15 = Utf8 VeCharm
      #16 = Utf8 com/vecharm/lychee/sample/api/Test
      #17 = Utf8 java/lang/Object
    ...
    SourceFile: "Test.java"    複製代碼

    • 運行時常量池。每個類都分配一個運行時常量池,用來保存類的一些數據,按照類型分類。
      • 常見的常量池的數據項類型:
        CONSTANT_Utf8
        UTF-8編碼的Unicode字符串
        CONSTANT_Integer
        int類型字面值
        CONSTANT_Float
        float類型字面值
        CONSTANT_Long
        long類型字面值
        CONSTANT_Double
        double類型字面值
        CONSTANT_Class
        對一個類或接口的符號引用
        CONSTANT_String
        String類型字面值
        CONSTANT_Fieldref
        對一個字段的符號引用
        CONSTANT_Methodref
        對一個類中聲明的方法的符號引用
        CONSTANT_InterfaceMethodref
        對一個接口中聲明的方法的符號引用
        CONSTANT_NameAndType
        對一個字段或方法的部分符號引用
    • 編譯後的代碼。一個Java類被編譯成class代碼,編譯的時候並不能肯定類的地址,只能用符號代替,編譯後的class文件,在ClassLoad而以後將會被提取分類保存在方法區,方法區保存的是類的信息,堆中保存的是類的對象,obj.getClass獲取的信息就是在方法區的。方法區也會溢出,當方法區的信息超過了閾值也會OOM,好比使用動態代理MethodInterceptor。

看到這想必就已經知道了一個從一個Java文件到內存是如何運做的了。類從加載到虛擬機內存中開始到卸載內存爲止,它的整個生命週期包括:加載,驗證,準備,解析,初始化,使用,和卸載7個階段,其中驗證,準備,解析3個部分被稱爲鏈接。數據庫

加載,驗證,準備,初始化和卸載這5個階段是肯定的,類的加載過程是必須按照順序來,而解析階段這個能夠在初始化以後開始,這是爲了支持運行時綁定(動態綁定)。編程

  1. 遇到new,getStatic,putStatic,invokeStatic這4條指令時,若是沒有初始化,則須要先觸發器初始化。
  2. 反射類的時候,會去常量池查查,若是沒有就會加載,初始化。
  3. 當初始化一個類的,做爲一個它的父類,若是沒有初始化就會先進行初始化。
  4. 當虛擬機啓動時,會先初始化用戶指定的主類。
  5. 使用MethodHandle。

說到底,編程就是編的只是數據和指令,來總結一下流程。api

  1. 經過一個類的全限定名來獲取定義此類的二進制字節流。
  2. 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
  3. 在內存中生存一個表明這個類的java.lang.Class對象,做爲方法區這個類各類數據的訪問入口。這個對象比較特殊,它存在方法區,不在堆區。並設置加載此class的ClassLoader引用。
  4. 驗證。驗證代碼的安全性。這個階段是否嚴謹,直接決定了Java虛擬機是否能承受惡意代碼的攻擊。
  5. 準備。正式爲類變量分配內存並設置類變量的初始值的階段,這些變量所使用的內存都將在方法區進行分配,這時候分配的變量都是靜態變量,不是實例變量,實例變量會在對象實例化時隨着對象一塊兒分配在Java堆中。
  6. 解析。解析階段時虛擬將常量池內的符號引用替換爲直接引用的過程。符號引用與虛擬機實現的內存佈局無關,引用的目標並不必定加載到內存中,同一個符號引用在不一樣虛擬機實例上翻譯出來的直接引用通常不會相同。
  7. 初始化。靜態方法使用<cinit>,實例對象使用<init>,對象存進Java堆。
  8. 尋找main方法執行,以後就是一個方法堆着一個方法的用了。

4、垃圾回收機制

在內存模型中,咱們須要重點關注的就是Heap。由於它是由咱們來控制的,處理不當容易發生OOM。內存處理的步驟無非也就三個: 申請,整理,清除。管理內存打個比方就和管理賣戲票的,觀衆臺也就幾十個座位,都是寶貴的資源。vip大戶,裏邊走,直接坐貴席。其餘的買計時票看,每隔必定時間把到時的人清出去,但常常有人到時賴着不走,隔一段時間催他才走。有時候座滿了,只能把到時的賴皮清出去,不想走能夠交錢。有時候人家成羣結隊的買票,天然要調配一下,清理出一些連座的給客戶對吧。若是是一大幫人來看,更是歡迎,vip裏面請。緩存

在Java的堆模型中劃分爲三個區。

  1. 新生代。這個區域的對象活動頻繁,朝生暮死的。能活下來的對象最終會被轉移到老年代,爲了管理這些對象,新生代還進行更細的劃分。
    • Eden
    • From Survivor
    • To Survivor
  2. 老年代。這個區域的對象存活比較久。通常能在GC下躲過15次的對象都會保存到這裏。若是申請大內存空間的對象,也是直接分配到這裏。分配擔保,最壞的狀況,新生代沒有足夠的內存分配,則會分配到老年代,固然也會分析老年代剩餘空間,判斷是否要進行一次Full GC。

管理對象的生命週期

生存仍是毀滅,是經過這個對象到GC Root的可到達性來決定的。能做爲GC Root的對象有四種。

  • 虛擬機棧引用的對象
  • 方法區中常量引用的對象
  • 方法區中靜態屬性引用的變量
  • 本地方法棧中native方法引用的對象

引用類型有四種,強引用,軟引用,弱引用,虛引用。

  1. 新生代對象的整理--複製整理法。這個區域因爲活動頻繁,容易更快的產生內存碎片,整理的時候還不能有大動做,因此這裏使用複製法,對cpu停頓小,代價是佔用必定的空間。
    • 若是發生Minor GC的時候,將Eden 存活下來的對象複製到 From Survivor ,對象在From Survivor每躲過一次GC 年齡就會+1,達到必定的程度,就會被移動到老年代,不然還沒死的話,就會移動到To Survivor ,若是To Survivor放不下了,這個對象會被移動到老年代。最後清空Eden 和 From Survivor,接着將To 和 From 交換,當To Survivor滿了就會將這些移動到老年代。
    • 如何保證新生代對象被老年代引用的時候不被gc?有些新生代對象會被老年代對象引用,然而老年代空間很大,若是每次Minor GC 都掃描一遍老年代,效率將大大下降,全部在老年代會劃分一個小區域來管理卡表,這寫卡表記錄了老年代和新生代的引用,也就是說這些老年代被當成新生代的GC roots。
  2. 老年代對象的整理--標記整理法。這個區域整理的時間間隔比較長,由於它們都是比較長久的數據,因此可使用標記法來處理,但對cpu停頓大。
    • 初始化標記。這時候會暫停「全世界(stop-the-world)」,開始進行標記。僅僅標記GC Roots能直接關聯到的對象。
    • 併發標記。從GC Roots開始進行可達性分析,找出存活對象,耗時長,就是進行追蹤引用鏈的過程,可與用戶線程併發執行。
    • 從新標記。修正併發標記階段因用戶線程繼續運行而致使標記發生變化的那部分對象的標記記錄。這個階段也會再次暫停全部事件。
    • 並行清理。最後執行清理,這個階段也是並行的。


結語,限於篇幅,只是初略的整理了一下大體的流程,參考《深刻Java虛擬機》等。
相關文章
相關標籤/搜索