中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

當前位置: 首頁 > news >正文

沈陽網(wǎng)站制作列表網(wǎng)整站seo教程

沈陽網(wǎng)站制作列表網(wǎng),整站seo教程,做網(wǎng)站軟件要錢嗎,張家港專業(yè)網(wǎng)站建設最近找工作,復習了下java相關(guān)的知識。發(fā)現(xiàn)已經(jīng)對很多概念模糊了。記錄一下。部分是往年面試題重新整理,部分是自己面試遇到的問題。持續(xù)更新中~ 目錄 java相關(guān)1. 面向?qū)ο笤O計原則2. 面向?qū)ο蟮奶卣魇鞘裁?. 重載和重寫4. 基本數(shù)據(jù)類型5. 裝箱和拆箱6. …

最近找工作,復習了下java相關(guān)的知識。發(fā)現(xiàn)已經(jīng)對很多概念模糊了。記錄一下。部分是往年面試題重新整理,部分是自己面試遇到的問題。持續(xù)更新中~

目錄

  • java相關(guān)
    • 1. 面向?qū)ο笤O計原則
    • 2. 面向?qū)ο蟮奶卣魇鞘裁?/li>
    • 3. 重載和重寫
    • 4. 基本數(shù)據(jù)類型
    • 5. 裝箱和拆箱
    • 6. final 有什么作用
    • 7. String是基本類型嗎,可以被繼承嗎
    • 8. String、StringBuffer和StringBuilder的區(qū)別?
    • 8. 抽象類和接口的區(qū)別
    • 9. String 類的常用方法都有那些?
    • 10. Java 中 IO 流分為幾種?
    • 11. Java容器有哪些
    • 12. Collection 和 Collections 有什么區(qū)別?
    • 13. HashMap 和 Hashtable 有什么區(qū)別?
    • 14. 如何決定使用 HashMap 還是 TreeMap?
    • 15. 說一下 HashMap 的實現(xiàn)原理?
    • 16. equals和 == 的區(qū)別
    • 17. ConcurrentHashMap如何實現(xiàn)線程安全的
    • 18. 說一下 HashSet 的實現(xiàn)原理?
    • 19. ArrayList 和 LinkedList 的區(qū)別是什么?
    • 20. ArrayList 和 Vector 的區(qū)別是什么?
    • 21. Array 和 ArrayList 有何區(qū)別?
    • 22. 在 Queue 中 poll()和 remove()有什么區(qū)別?
    • 多線程
    • 23. 并行和并發(fā)有什么區(qū)別?
    • 24. 線程和進程的區(qū)別?
    • 25. 守護線程是什么?
    • 26. 創(chuàng)建線程有哪幾種方式?
    • 27. 說一下 runnable 和 callable 有什么區(qū)別?
    • 28. 線程有哪些狀態(tài)?
    • 29. sleep() 和 wait() 有什么區(qū)別?
    • 30. notify()和 notifyAll()有什么區(qū)別?
    • 31. 線程的 run() 和 start() 有什么區(qū)別?
    • 32. 為什么要使用線程池
    • 33. 創(chuàng)建線程池有哪幾種方式?
    • 34. ThreadPoolExecutor了解嗎?參數(shù)是什么意思
    • 35. 線程池中 submit() 和 execute() 方法有什么區(qū)別?
    • 36. 線程池都有哪些狀態(tài)?
    • 37. 知道線程池中線程復用原理嗎?
    • 38. 什么是死鎖?
    • 39. ThreadLocal 是什么?有哪些使用場景?
    • 40. 說一下 synchronized 底層實現(xiàn)原理?
    • 41. synchronized 和 volatile 的區(qū)別是什么?
    • 42. synchronized 和 Lock 有什么區(qū)別?
    • 43. synchronized 和 ReentrantLock 區(qū)別是什么?
    • 44. 說一下 atomic 的原理?
    • 45. 什么是反射?
    • 46. 創(chuàng)建對象有幾種方式
    • 47. 使用過哪些設計模式?
    • 48. 線程間如何通信?
    • JVM相關(guān)
    • 1. 簡單介紹下jvm虛擬機模型
    • 2. 類加載器子系統(tǒng)
    • 3.運行時數(shù)據(jù)區(qū)
    • 4. 執(zhí)行引擎
    • 5. 了解GC嗎
    • 6. 了解JMM嗎
    • 7. 類的加載過程,Person person = new Person();為例進行說明。
  • kotlin
    • 1. Kotlin如何實現(xiàn)空安全的?
    • 2. 談談你對協(xié)程的理解
    • 3. 了解密封類(Sealed Classes)嗎
    • 3. Kotlin中@JvmOverloads 的作用
    • 4.Kotlin實現(xiàn)單例的幾種方式
    • 5. 了解Data Class嗎?
    • 6. 了解作用域函數(shù)嗎?
    • 7. 你覺得Kotlin與Java混合開發(fā)時需要注意哪些問題?
    • 8. 知道什么是inline ,noinline和crossinline函數(shù)嗎?
    • 9. Kotlin中的構(gòu)造方法
    • 10. 說說Kotlin中的Any與Java中的Object有何異同?
    • 11. 協(xié)程Flow是什么,有哪些應用場景?
    • 12. 協(xié)程Flow的冷流和熱流是什么?
    • 13. 談談Kotlin中的Sequence,為什么它處理集合操作更加高效?
  • android
    • 1. Activity啟動模式
    • 2. Activity生命周期
    • 3. 了解Service嗎
    • 4. 使用過broadcastReceiver嗎?
    • 5. 說說你對handler的理解
      • 如何使用Handler?
      • 主線程使用Handler為什么不用Looper.prepare()?
      • 簡述一下Handler的工作流程
      • 一個線程中最多有多少個Handler,Looper,MessageQueue?
      • Looper死循環(huán)為什么不會導致應用ANR、卡死,會耗費大量資源嗎?
      • Handler同步屏障了解嗎
      • Handler 為什么可能導致內(nèi)存泄露?如何避免?
      • Handler是如何實現(xiàn)線程間通訊的
      • Handler消息處理的優(yōu)先級
      • 如何正確或Message實例
      • Android 為什么不允許并發(fā)訪問 UI?
      • 了解ThreadLocal嗎
      • ThreadLocal與內(nèi)存泄漏
      • Message 的執(zhí)行時刻如何管理
      • Looper等待如何準確喚醒的?
      • Handler機制原理
    • 6. 了解View繪制流程嗎?
    • 7. 自定義View流程是什么
    • 8. 了解View事件分發(fā)機制嗎
    • 9. ListVie和RecycleView的區(qū)別
      • 1. 優(yōu)化
      • 2. 布局不同
      • 3. 更新數(shù)據(jù)
      • 4. 自定義適配器
      • 5. 綁定事件不同
    • 10. 展開講講recycleView
      • recycleView的緩存了解嗎
      • 問題1. RecyclerView第一次layout時,會發(fā)生預布局pre-layout嗎?
      • 問題2. 如果自定義LayoutManager需要注意什么?
      • 問題3. CachedView和RecycledViewPool兩者區(qū)別
      • 問題4. 你是從哪些方面優(yōu)化RecyclerView的?
    • 11. 你知道IPC嗎?
    • 12. 展開說說Binder
      • 問題1. Binder實現(xiàn)原理是什么
    • 13. 了解MVC,MVP,MVVM嗎?
    • 14. 使用過jetpack庫嗎?
      • 1. LifeCycle原理
      • 問題 1. LifeCycle怎么做到監(jiān)聽LifecycleOwner(Activity或Fragment)的生命周期的?
      • 2. Room
      • 3. LiveData
        • observe和observeForever區(qū)別
        • LiveData粘性事件和數(shù)據(jù)倒灌
      • 4. DataBinding
    • 15. launcher頁面點擊app圖標啟動過程了解嗎?以點擊淘寶為例。
    • 16. 為什么用到socket又用到Binder,為什么不統(tǒng)一用binder呢?
    • 17. context 和 activity的區(qū)別
    • 18. 一個應用程序中有多少個context?
    • 開源框架篇
    • 1. OKHTTP了解嗎?
      • 問題 1. 知道OkHttp有幾個攔截器以及作用嗎嗎?
      • 問題 2. OkHttp怎么實現(xiàn)連接池
      • 問題 3. 簡述一下OkHttp的一個工作流程
      • 問題 4. Okhttp 如何實現(xiàn)緩存功能?它是如何根據(jù) Cache-Control 首部來判斷緩存策略的?
      • 問題 5. Okhttp 如何自定義攔截器?你有沒有使用過或編寫過自己的攔截器?
      • 問題 6. Okhttp 如何管理連接池和線程池?它是如何復用和回收連接的?
    • 2. Glide了解嗎?
    • 3. EventBus了解嗎?
    • 架構(gòu)方面
    • 1. 組件化
      • 聊聊你對Arouter的理解
      • APT是什么
      • 什么是注解?有哪些注解?
    • 2. 插件化
    • Flutter
    • 1. dart中的作用域與了解嗎
    • 2. dart中. .. ...分別是什么意思?
    • 3. Dart 是不是單線程模型?如何運行的?
    • 4. Dart既然是單線程模型支持多線程嗎?
    • 5. Future是什么
    • 6. Stream是什么
    • 7. Flutter 如何和原生交互
    • 8. 說一下 mixin?
    • 9. StatefulWidget 的生命周期
    • 10. main()和runApp()函數(shù)在flutter的作用分別是什么?有什么關(guān)系嗎?
    • 11. 怎么理解Isolate?
    • 12. 簡單介紹下Flutter框架,以及它的優(yōu)缺點?
    • 12. 簡述Widgets、RenderObjects 和 Elements的關(guān)系
    • 13. 介紹下Widget、State、Context 概念
    • 14. 簡述Widget的StatelessWidget和StatefulWidget兩種狀態(tài)組件類
    • 15. 什么是狀態(tài)管理,你了解哪些狀態(tài)管理框架?
    • 16. 簡述Flutter的繪制流程
    • 17. await for 如何使用?
    • 18. 介紹下Flutter的架構(gòu)
    • 19. 介紹下Flutter的FrameWork層和Engine層,以及它們的作用
    • 20. Dart中var與dynamic的區(qū)別
    • 21. const關(guān)鍵字與final關(guān)鍵字的區(qū)別
    • 22. Flutter在Debug和Release下分別使用什么編譯模式,有什么區(qū)別?
    • 23. 什么是Key?
    • 24. future 和steam有什么不一樣?
    • 25. 什么是widget? 在flutter里有幾種類型的widget?
    • 26. statefulWidget更新流程了解嗎

java相關(guān)

1. 面向?qū)ο笤O計原則

  • 面向?qū)ο笤O計原則
  • 單一職責原則——SRP
    一個類的職責盡量單一,清晰。即一個類最好專注做一件事情,而不是分散的做好幾件事。
    每個類都只負責一項任務,可以降低類的復雜性;提高可讀性;提高系統(tǒng)可維護性;避免類的臃腫和功能太多太復雜。
  • 依賴倒置原則——DIP
    實現(xiàn)時盡量依賴抽象,而不依賴于具體實現(xiàn)。
    可以減少類間的耦合性,提高系統(tǒng)穩(wěn)定性
    提高代碼的可讀性,可維護性以及擴展性。
  • 接口隔離原則——ISP
    即應當為客戶端提供盡可能小的單獨的接口,而不是大而雜的接口。
    也就是要為各個類建立專用的接口,而不要試圖去建立一個很龐大的接口供所有依賴它的類去調(diào)用。依賴幾個專用的接口要比依賴一個綜合的接口更靈活。
  • 里氏替換原則——LSP
    即超類存在的地方,子類是可以替換的。
    里氏替換原則是實現(xiàn)開閉原則的重要方式之一,由于使用基類對象的地方都可以使用子類對象,因此在程序中盡量使用基類類型來對對象進行定義,而在運行時再確定其子類類型,用子類對象來替換父類對象。
  • 迪米特原則——LOD
    即一個軟件實體應當盡可能少的與其他實體發(fā)生相互作用。
    一個對象對另一個對象知道的越少越好,即一個軟件實體應當盡可能少的與其他實體發(fā)生相互作用,在一個類里能少用多少其他類就少用多少,尤其是局部變量的依賴類,能省略盡量省略。
  • 開閉原則——OCP
    面向修改關(guān)閉,面向擴展開放。
    即一個軟件、一套系統(tǒng)在開發(fā)完成后,當有增加或修改需求時,應該對拓展代碼打開,對修改原有代碼關(guān)閉

2. 面向?qū)ο蟮奶卣魇鞘裁?/h2>
  • 封裝
    把客觀事物封裝成抽象的類,并且類可以把自己的數(shù)據(jù)和方法只讓可信的類或者對象操作,對不可信的進行信息隱藏。
  • 繼承
    繼承是指這樣一種能力:它可以使用現(xiàn)有類的所有功能,并在無需重新編寫原來的類的情況下對這些功能進行擴展。
    通過繼承創(chuàng)建的新類稱為“子類”或“派生類”。
    被繼承的類稱為“基類”、“父類”或“超類”。
    繼承的過程,就是從一般到特殊的過程。
    要實現(xiàn)繼承,可以通過“繼承”(Inheritance)和“組合”(Composition)來實現(xiàn)。
  • 多態(tài)
    就是指一個類實例的相同方法在不同情形有不同表現(xiàn)形式。多態(tài)機制使具有不同內(nèi)部結(jié)構(gòu)的對象可以共享相同的外部接口。
    實現(xiàn)多態(tài)一般通過重寫重載

封裝可以隱藏實現(xiàn)細節(jié),使得代碼模塊化;繼承可以擴展已存在的代碼模塊(類);它們的目的都是為了——代碼重用。而多態(tài)則是為了實現(xiàn)另一個目的——接口重用!多態(tài)的作用,就是為了類在繼承和派生的時候,保證使用“家譜”中任一類的實例的某一屬性時的正確調(diào)用。

3. 重載和重寫

  • 重寫
    是指子類重新定義父類的虛函數(shù)的做法。需要保持參數(shù)個數(shù),類型,返回類型完全一致。
    屬于運行時多態(tài)的表現(xiàn)
  • 重載
    是指允許存在多個同名函數(shù),而這些函數(shù)的參數(shù)表不同(或許參數(shù)個數(shù)不同,或許參數(shù)類型不同,或許兩者都不同)
    其實,重載的概念并不屬于“面向?qū)ο缶幊獭?#xff0c;重載的實現(xiàn)是:編譯器根據(jù)函數(shù)不同的參數(shù)表,對同名函數(shù)的名稱做修飾,然后這些同名函數(shù)就成了不同的函數(shù)(至少對于編譯器來說是這樣的)。如,有兩個同名函數(shù):function func(p:integer):integer;和function func(p:string):integer;。那么編譯器做過修飾后的函數(shù)名稱可能是這樣的:int_func、str_func。對于這兩個函數(shù)的調(diào)用,在編譯器間就已經(jīng)確定了,是靜態(tài)的(記住:是靜態(tài))

4. 基本數(shù)據(jù)類型

數(shù)據(jù)類型位數(shù)默認值取值范圍示例
byte1字節(jié)0-2(128~127)byte a = 10;
short2字節(jié)0-2(-32768~32767)short a = 10;
int4字節(jié)0(-2147483648~2147483647)int a = 10;
float4字節(jié)0.0-2(-2147483648~2147483647)float a = 10;
long8字節(jié)0-2(-9223372036854774808~9223372036854774807)long a = 10;
double8字節(jié)0.0-2(-9223372036854774808~9223372036854774807)char a = 10;
char2字節(jié)-2(128~127)char a = ‘a(chǎn)’;
boolean1字節(jié)falsetrue、falsebooleana = true;
特殊類型void

對應的包裝類
byte——Byte
short——Short
int ——Integer
float——Float
long——Long
double——Double
char——Character
boolean——Boolean
void——Void

包裝類出現(xiàn)的原因是Java語言是面對對象的編程語言,而基本數(shù)據(jù)類型聲明的變量并不是對象,為其提供包裝類,增強了Java面向?qū)ο蟮男再|(zhì)。
void是一個特殊的類型,有人把它歸到Java基本數(shù)據(jù)類型中,是因為可以通過Class.getPrimitiveClass(“void”)獲取對應的原生類型。
void有個對應的類型Void,可以把它看做是void的包裝類,Void的作用主要作用有以下2個:

  • 泛型占位
    當我們定義了泛型時,如果沒有寫泛型參數(shù),idea等開發(fā)工具會給出提醒,建議寫上泛型類型,但實際上卻是不需要固定的泛型類型,這時候據(jù)可以寫上Void來消除警告,例如ResponseData
  • 反射獲取void方法
Method[] methods = String.class.getMethods();
for (Method method : methods) {if(method.getGenericReturnType().equals(Void.TYPE)){System.out.println(method.getName());}
}
//輸出:
//getBytes
//getChars
//wait
//wait
//wait
//notify
//notifyAll

5. 裝箱和拆箱

將基本數(shù)據(jù)類型轉(zhuǎn)化為包裝類就叫做裝箱;
調(diào)用 包裝類.valueOf()方法

     int a = 22;//裝箱   在實例化時候進行裝箱Integer inter1 = new Integer(a);//裝箱  調(diào)用valueOf方法進行裝箱Integer inter2 = Integer.valueOf(a);valueOf 方法是一個靜態(tài)方法,直接通過類進行調(diào)用 

拆箱
將包裝類轉(zhuǎn)化為基本數(shù)據(jù)類型;
調(diào)用 包裝類.parseXXX()方法

int a = Integer.parseInt("3");

6. final 有什么作用

  • final 修飾的類叫最終類,該類不能被繼承。
  • final 修飾的方法不能被重寫。
  • final 修飾的變量叫常量,常量必須初始化,初始化之后值就不能被修改。

7. String是基本類型嗎,可以被繼承嗎

String不是基本類型,不可以被繼承。因為String被final關(guān)鍵字修飾,不可以被繼承

public final class String implements Serializable, Comparable<String>, CharSequence {

8. String、StringBuffer和StringBuilder的區(qū)別?

String 大小固定數(shù)不可變的。
因為String是字符串常量,是不可變的。實際的拼接操作最后都是產(chǎn)生了一個新的對象并存儲這個結(jié)果

String str1 = "123";
String str2 = "123"//實際上 "123"這個字符串在常量池中只有一份,str1,str2 兩個對象都是指向"123"
str2 = str2 + "45";
實際上是產(chǎn)生了個新的String對象存放"12345"這個結(jié)果
查看字節(jié)碼實際代碼是0 ldc #2 <123>2 astore_13 new #3 <java/lang/StringBuilder>6 dup7 invokespecial #4 <java/lang/StringBuilder.<init> : ()V>
10 aload_1
11 invokevirtual #5 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
14 ldc #6 <45>
16 invokevirtual #5 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
19 invokevirtual #7 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
22 astore_1
23 return
//實際也是通過 StringBuilder.append方法實現(xiàn)拼接,然后toString的性能比較低
//所以字符串拼接直接使用StringBuilder實現(xiàn)效率更高

StringBuffer 大小可變,線程安全(有鎖),同步,效率低,適用于多線程,低并發(fā)。
StringBuilder 大小可變,線程不安全(無鎖),不同步,效率高,適用于單線程,高并發(fā)。

8. 抽象類和接口的區(qū)別

  • 抽象類是對事物屬性的抽象,接口是對行為的抽象,是一種規(guī)范或者說行為約束。
  • 抽象類關(guān)鍵詞abstract 接口關(guān)鍵字interface
  • 抽象類可以有成員變量,普通方法和抽象方法,接口只能有抽象方法(只有方法定義,無具體函數(shù)實現(xiàn))
  • 抽象類可以有構(gòu)造方法,接口沒有構(gòu)造方法
  • 繼承了抽象類的子類,要么對父類的抽象方法進行重寫,要么自己也是抽象類
  • 抽象類的子類使用 extends 來繼承;接口必須使用 implements 來實現(xiàn)接口。
  • 實現(xiàn)數(shù)量:類可以實現(xiàn)很多個接口;但是只能繼承一個抽象類。
  • 訪問修飾符:接口中的方法默認使用 public 修飾;抽象類中的方法可以是任意訪問修飾符。
  • 9. String 類的常用方法都有那些?

  • inexOf():返回指定字符的索引。
  • charAt():返回指定索引處的字符。
  • replace():字符串替換。
  • trim():去除字符串兩端空白。
  • split():分割字符串,返回一個分割后的字符串數(shù)組。
  • getBytes():返回字符串的 byte 類型數(shù)組。
  • length():返回字符串長度。
  • toLowerCase():將字符串轉(zhuǎn)成小寫字母。
  • toUpperCase():將字符串轉(zhuǎn)成大寫字符。
  • substring():截取字符串。
  • equals():字符串比較。
  • 10. Java 中 IO 流分為幾種?

    按功能來分:輸入流(input)、輸出流(output)。
    按類型來分:字節(jié)流和字符流。
    字節(jié)流和字符流的區(qū)別是:字節(jié)流按 8 位傳輸以字節(jié)為單位輸入輸出數(shù)據(jù),字符流按 16 位傳輸以字符為單位輸入輸出數(shù)據(jù)。

    11. Java容器有哪些

    Java 容器分為 Collection 和 Map 兩大類,其下又有很多子類,如下所示:
  • Collection
    • List
      • ArrayList
      • LinkedList
      • Vector
      • Stack
    • Set
      • HashSet
      • LinkedHashSet
      • TreeSet
  • Map
    • HashMap
      • LinkedHashMap
    • TreeMap
    • ConcurrentHashMap
    • Hashtable

12. Collection 和 Collections 有什么區(qū)別?

  • Collection 是一個集合接口,它提供了對集合對象進行基本操作的通用接口方法,所有集合都是它的子類,比如 List、Set 等。
  • Collections 是一個包裝類,包含了很多靜態(tài)方法,不能被實例化,就像一個工具類,比如提供的排序方法:Collections. sort(list)。

13. HashMap 和 Hashtable 有什么區(qū)別?

  • 存儲:HashMap 允許 key 和 value 為 null,而 Hashtable 不允許。
  • 線程安全:Hashtable 是線程安全的,而 HashMap 是非線程安全的。
  • 推薦使用:在 Hashtable 的類注釋可以看到,Hashtable 是保留類不建議使用,推薦在單線程環(huán)境下使用 HashMap 替代,如果需要多線程使用則用 ConcurrentHashMap 替代。

14. 如何決定使用 HashMap 還是 TreeMap?

對于在 Map 中插入、刪除、定位一個元素這類操作,HashMap 是最好的選擇,因為相對而言 HashMap 的插入會更快,但如果你要對一個 key 集合進行有序的遍歷,那 TreeMap 是更好的選擇

15. 說一下 HashMap 的實現(xiàn)原理?

HashMap 基于 Hash 算法實現(xiàn)的,我們通過 put(key,value)存儲,get(key)來獲取。當傳入 key 時,HashMap 會根據(jù) key. hashCode() 計算出 hash 值,根據(jù) hash 值將 value 保存在 bucket 里。當計算出的 hash 值相同時,我們稱之為 hash 沖突,HashMap 的做法是用鏈表和紅黑樹存儲相同 hash 值的 value。當 hash 沖突的個數(shù)比較少時,使用鏈表否則使用紅黑樹。
jdk1.7以前使用數(shù)組+鏈表實現(xiàn)HashMap,jdk1.8以后用數(shù)組+紅黑樹實現(xiàn)。當鏈表長度較短時使用鏈表,長度達到閾值時自動轉(zhuǎn)換為紅黑樹,提高查詢效率。

  • 其它問題
  • 默認大小 16 ,負載因子0.75 大小到達12時 自動兩倍擴容。

16. equals和 == 的區(qū)別

  • 對于 == 來說:
    如果比較的是基本數(shù)據(jù)類型變量,比較兩個變量的值是否相等。(不一定數(shù)據(jù)類型相同)
    如果比較的是引用數(shù)據(jù)類型變量,比較兩個對象的地址值是否相同,即兩個引用是否指向同一個地址值
  • 對于 equals 來說:
    如果類中重寫了equals方法,比較內(nèi)容是否相等。
    String、Date、File、包裝類都重寫了Object類的equals方法。

如果類中沒有重寫equals方法,比較地址值是否相等(是否指向同一個地址值)。

Student stu1 = new Student(11, "張三");
Student stu2 = new Student(11,"張三");
System.out.println(stu1.equals(stu2));//false

既然equals比較的是內(nèi)容是否相同,為什么結(jié)果還是false呢?
回顧知識:
在Java中我們知道任何類的超類都是Object類,Student類也繼承Object類。
查看Object類中的equals方法也是 == 比較(也就是比較地址值),因此結(jié)果當然是false。

 public boolean equals(Object obj) {return (this == obj);}

既然這樣我們?nèi)绾伪WC兩個對象內(nèi)容相同呢?
這里就需要我們?nèi)ブ貙慹quals方法?

  @Overridepublic boolean equals(Object obj){if (this == obj){return true;}if (obj instanceof Student) {Student stu = (Student)obj;return this.age == stu.age && this.name.equals(stu.name);}return false;}

17. ConcurrentHashMap如何實現(xiàn)線程安全的

  • jdk 1.7以前結(jié)構(gòu)是segment數(shù)組 + HashEntry數(shù)組 + 鏈表,使用分段式鎖,實現(xiàn)線程安全。容器中有多把鎖,每一把鎖鎖一段數(shù)據(jù),這樣在多線程訪問時不同段的數(shù)據(jù)時,就不會存在鎖競爭了,這 樣便可以有效地提高并發(fā)效率。這就是ConcurrentHashMap所采用的”分段鎖”思想,見下圖:
    在這里插入圖片描述
    get()操作:
    HashEntry中的value屬性和next指針是用volatile修飾的,保證了可見性,所以每次獲取的都是最新值,get過程不需要加鎖。
    1.將key傳入get方法中,先根據(jù)key的hashcode的值找到對應的segment段。
    2.再根據(jù)segment中的get方法再次hash,找到HashEntry數(shù)組中的位置。
    3.最后在鏈表中根據(jù)hash值和equals方法進行查找。
    ConcurrentHashMap的get操作跟HashMap類似,只是ConcurrentHashMap第一次需要經(jīng)過一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍歷該HashEntry下的鏈表進行對比,成功就返回,不成功就返回null。
    put()操作:
    1.將key傳入put方法中,先根據(jù)key的hashcode的值找到對應的segment段
    2.再根據(jù)segment中的put方法,加鎖lock()。
    3.再次hash確定存放的hashEntry數(shù)組中的位置
    4.在鏈表中根據(jù)hash值和equals方法進行比較,如果相同就直接覆蓋,如果不同就插入在鏈表中。

  • jdk1.8以后結(jié)構(gòu)是 數(shù)組+Node+紅黑樹實現(xiàn),采用**Synchronized + CAS(自旋鎖)**保證線程安全。Node的val和next都用volatile保證,保證可見性,查找,替換,賦值操作都使用CAS

為什么在有Synchronized 的情況下還要使用CAS
因為CAS是樂觀鎖,在一些場景中(并發(fā)不激烈的情況下)它比Synchronized和ReentrentLock的效率要高,當CAS保障不了線程安全的情況下(擴容或者hash沖突的情況下)轉(zhuǎn)成Synchronized
來保證線程安全,大大提高了低并發(fā)下的性能.

鎖 :
鎖是鎖的鏈表的head的節(jié)點,不影響其他元素的讀寫,鎖粒度更細,效率更高,擴容時,阻塞所有的讀寫操作(因為擴容的時候使用的是Synchronized鎖,鎖全表),并發(fā)擴容.

讀操作無鎖 :

  • Node的val和next使用volatile修飾,讀寫線程對該變量互相可見

  • 數(shù)組用volatile修飾,保證擴容時被讀線程感知

  • get()操作:
    get操作全程無鎖。get操作可以無鎖是由于Node元素的val和指針next是用volatile修飾的。
    在多線程環(huán)境下線程A修改節(jié)點的val或者新增節(jié)點的時候是對線程B可見的。
    1.計算hash值,定位到Node數(shù)組中的位置
    2.如果該位置為null,則直接返回null
    3.如果該位置不為null,再判斷該節(jié)點是紅黑樹節(jié)點還是鏈表節(jié)點
    如果是紅黑樹節(jié)點,使用紅黑樹的查找方式來進行查找
    如果是鏈表節(jié)點,遍歷鏈表進行查找

  • put()操作:
    1.先判斷Node數(shù)組有沒有初始化,如果沒有初始化先初始化initTable();
    2.根據(jù)key的進行hash操作,找到Node數(shù)組中的位置,如果不存在hash沖突,即該位置是null,直接用CAS插入
    3.如果存在hash沖突,就先對鏈表的頭節(jié)點或者紅黑樹的頭節(jié)點加synchronized鎖
    4.如果是鏈表,就遍歷鏈表,如果key相同就執(zhí)行覆蓋操作,如果不同就將元素插入到鏈表的尾部, 并且在鏈表長度大于8, Node數(shù)組的長度超過64時,會將鏈表的轉(zhuǎn)化為紅黑樹。
    5.如果是紅黑樹,就按照紅黑樹的結(jié)構(gòu)進行插入。

18. 說一下 HashSet 的實現(xiàn)原理?

  • HashSet 是基于 HashMap 實現(xiàn)的,HashSet 底層使用 HashMap 來保存所有元素,因此 HashSet 的實現(xiàn)比較簡單,相關(guān) HashSet 的操作,基本上都是直接調(diào)用底層 HashMap 的相關(guān)方法來完成,HashSet 不允許重復的值。

19. ArrayList 和 LinkedList 的區(qū)別是什么?

  • 數(shù)據(jù)結(jié)構(gòu)實現(xiàn):ArrayList 是動態(tài)數(shù)組的數(shù)據(jù)結(jié)構(gòu)實現(xiàn),而 LinkedList 是雙向鏈表的數(shù)據(jù)結(jié)構(gòu)實現(xiàn)。
  • 隨機訪問效率:ArrayList 比 LinkedList 在隨機訪問的時候效率要高,因為 LinkedList 是線性的數(shù)
  • 存儲方式,所以需要移動指針從前往后依次查找。
  • 增加和刪除效率:在非首尾的增加和刪除操作,LinkedList 要比 ArrayList 效率要高,因為 ArrayList 增刪操作要影響數(shù)組內(nèi)的其他數(shù)據(jù)的下標。

20. ArrayList 和 Vector 的區(qū)別是什么?

  • 線程安全:Vector 使用了 Synchronized 來實現(xiàn)線程同步,是線程安全的,而 ArrayList 是非線程安全的。
  • 性能:ArrayList 在性能方面要優(yōu)于 Vector。
  • 擴容:ArrayList 和 Vector 都會根據(jù)實際的需要動態(tài)的調(diào)整容量,只不過在 Vector 擴容每次會增加 1 倍,而 ArrayList 只會增加 50%。

21. Array 和 ArrayList 有何區(qū)別?

  • Array 可以存儲基本數(shù)據(jù)類型和對象,ArrayList 只能存儲對象。
  • Array 是指定固定大小的,而 ArrayList 大小是自動擴展的。
  • Array 內(nèi)置方法沒有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。

22. 在 Queue 中 poll()和 remove()有什么區(qū)別?

  • 相同點:都是返回第一個元素,并在隊列中刪除返回的對象。
  • 不同點:如果沒有元素 poll()會返回 null,而 remove()會直接拋出 NoSuchElementException 異常。

多線程

23. 并行和并發(fā)有什么區(qū)別?

  • 并行:多個處理器或多核處理器同時處理多個任務。
  • 并發(fā):多個任務在同一個 CPU 核上,按細分的時間片輪流(交替)執(zhí)行,從邏輯上來看那些任務是同時執(zhí)行。

24. 線程和進程的區(qū)別?

  • 進程是cpu資源分配的基本單位
  • 線程是cpu調(diào)度和執(zhí)行的最小單位
  • 一個程序下至少有一個進程,一個進程下至少有一個線程,一個進程下也可以有多個線程來增加程序的執(zhí)行速度。

25. 守護線程是什么?

  • 守護線程是運行在后臺的一種特殊線程。它獨立于控制終端并且周期性地執(zhí)行某種任務或等待處理某些發(fā)生的事件。在 Java 中垃圾回收線程就是特殊的守護線程。

26. 創(chuàng)建線程有哪幾種方式?

  • 繼承 Thread 重寫 run 方法;
  • 實現(xiàn) Runnable 接口;
  • 實現(xiàn) Callable 接口。

27. 說一下 runnable 和 callable 有什么區(qū)別?

  • runnable 沒有返回值,callable 可以拿到有返回值,callable 可以看作是 runnable 的補充。

28. 線程有哪些狀態(tài)?

線程的狀態(tài):

  • NEW 尚未啟動
  • RUNNABLE 就緒態(tài)
  • RUNNING 運行中
  • BLOCKED 阻塞的(被同步鎖或者IO鎖阻塞)
  • WAITING 永久等待狀態(tài)
  • TIMED_WAITING 等待指定的時間重新被喚醒的狀態(tài)
  • TERMINATED 執(zhí)行完成
    在這里插入圖片描述

29. sleep() 和 wait() 有什么區(qū)別?

  • 類的不同:sleep() 來自 Thread,wait() 來自 Object。
  • 釋放鎖:sleep() 不釋放鎖;wait() 釋放鎖。
  • 用法不同:sleep() 時間到會自動恢復;wait() 可以使用 notify()/notifyAll()直接喚醒。

30. notify()和 notifyAll()有什么區(qū)別?

  • notifyAll()會喚醒所有的線程,notify()之后喚醒一個線程。notifyAll() 調(diào)用后,會將全部線程由等待池移到鎖池,然后參與鎖的競爭,競爭成功則繼續(xù)執(zhí)行,如果不成功則留在鎖池等待鎖被釋放后再次參與競爭。而 notify()只會喚醒一個線程,具體喚醒哪一個線程由虛擬機控制。

31. 線程的 run() 和 start() 有什么區(qū)別?

  • start() 方法用于啟動線程,run() 方法用于執(zhí)行線程的運行時代碼。run() 可以重復調(diào)用,而 start() 只能調(diào)用一次。
  • 直接調(diào)用run方法,不會在線程中執(zhí)行,只是相當于執(zhí)行了一個普通的方法。

32. 為什么要使用線程池

  • 降低資源消耗。通過重復利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗。
  • 提高響應速度。當任務到達時,任務可以不需要等到線程創(chuàng)建就能立即執(zhí)行。
  • 提高線程的可管理性。

33. 創(chuàng)建線程池有哪幾種方式?

線程池創(chuàng)建有七種方式,最核心的是最后一種:

  • newSingleThreadExecutor():它的特點在于工作線程數(shù)目被限制為 1,操作一個無界的工作隊列,所以它保證了所有任務的都是被順序執(zhí)行,最多會有一個任務處于活動狀態(tài),并且不允許使用者改動線程池實例,因此可以避免其改變線程數(shù)目;
  • newCachedThreadPool():它是一種用來處理大量短時間工作任務的線程池,具有幾個鮮明特點:它會試圖緩存線程并重用,當無緩存線程可用時,就會創(chuàng)建新的工作線程;如果線程閑置的時間超過 60 秒,則被終止并移出緩存;長時間閑置時,這種線程池,不會消耗什么資源。其內(nèi)部使用 SynchronousQueue 作為工作隊列;
  • newFixedThreadPool(int nThreads):重用指定數(shù)目(nThreads)的線程,其背后使用的是無界的工作隊列,任何時候最多有 nThreads 個工作線程是活動的。這意味著,如果任務數(shù)量超過了活動隊列數(shù)目,將在工作隊列中等待空閑線程出現(xiàn);如果有工作線程退出,將會有新的工作線程被創(chuàng)建,以補足指定的數(shù)目 nThreads;
  • newSingleThreadScheduledExecutor():創(chuàng)建單線程池,返回 ScheduledExecutorService,可以進行定時或周期性的工作調(diào)度;
  • newScheduledThreadPool(int corePoolSize):和newSingleThreadScheduledExecutor()類似,創(chuàng)建的是個 ScheduledExecutorService,可以進行定時或周期性的工作調(diào)度,區(qū)別在于單一工作線程還是多個工作線程;
  • newWorkStealingPool(int parallelism):這是一個經(jīng)常被人忽略的線程池,Java 8 才加入這個創(chuàng)建方法,其內(nèi)部會構(gòu)建ForkJoinPool,利用Work-Stealing算法,并行地處理任務,不保證處理順序;
  • ThreadPoolExecutor():是最原始的線程池創(chuàng)建,上面1-3創(chuàng)建方式都是對ThreadPoolExecutor的封裝。

34. ThreadPoolExecutor了解嗎?參數(shù)是什么意思

  • corePoolSize: 線程池中的核心線程數(shù),默認情況下核心線程一直存活在線程池中,如果將ThreadPoolExecutor 的 allowCoreThreadTimeOut 屬性設為 true,如果線程池一直閑置并超過了 keepAliveTime 所指定的時間,核心線程就會被終止。
  • maximumPoolSize: 最大線程數(shù),當線程不夠時能夠創(chuàng)建的最大線程數(shù)(包含核心線程數(shù))

臨時線程數(shù) = 最大線程數(shù) - 核心線程數(shù)

  • keepAliveTime: 線程池的閑置超時時間,默認情況下對非核心線程生效,如果閑置時間超過這個時間,非核心線程就會被回收。如果 ThreadPoolExecutor 的 allowCoreThreadTimeOut 設為 true 的時候,核心線程如果超過閑置時長也會被回收。
  • unit: 配合 keepAliveTime 使用,用來標識 keepAliveTime 的時間單位。
  • workQueue: 線程池中的任務隊列,使用 execute() 或 submit() 方法提交的任務都會存儲在此隊列中。
  • threadFactory: 為線程池提供創(chuàng)建新線程的線程工廠。
  • rejectedExecutionHandler: 線程池任務隊列超過最大值之后的拒絕策略, RejectedExecutionHandler 是一個接口,里面只有一個rejectedExecution方法,可在此方法內(nèi)添加任務超出最大值的事件處理;

ThreadPoolExecutor 也提供了 4 種默認的拒絕策略:

  • DiscardPolicy():丟棄掉該任務但是不拋出異常,不推薦這種(導致使用者沒覺察情況發(fā)生)
  • DiscardOldestPolicy():丟棄隊列中等待最久的任務,然后把當前任務加入隊列中。
  • AbortPolicy():丟棄任務并拋出 RejectedExecutionException 異常(默認)。
  • CallerRunsPolicy():由主線程負責調(diào)用任務的run()方法從而繞過線程池直接執(zhí)行,既不拋棄任務也不拋出異常(當最大線程數(shù)滿了,任務隊列中也滿了,再來一個任務,由主線程執(zhí)行)
    在這里插入圖片描述

35. 線程池中 submit() 和 execute() 方法有什么區(qū)別?

  • execute():只能執(zhí)行 Runnable 類型的任務。
  • submit():可以執(zhí)行 Runnable 和 Callable 類型的任務。
  • Callable 類型的任務可以獲取執(zhí)行的返回值,而 Runnable 執(zhí)行無返回值。

36. 線程池都有哪些狀態(tài)?

  • RUNNING:這是最正常的狀態(tài),接受新的任務,處理等待隊列中的任務。
  • SHUTDOWN:不接受新的任務提交,但是會繼續(xù)處理等待隊列中的任務。
  • STOP:不接受新的任務提交,不再處理等待隊列中的任務,中斷正在執(zhí)行任務的線程。
  • TIDYING:所有的任務都銷毀了,workCount 為 0,線程池的狀態(tài)在轉(zhuǎn)換為 TIDYING 狀態(tài)時,會執(zhí)行鉤子方法 terminated()。
  • TERMINATED:terminated()方法結(jié)束后,線程池的狀態(tài)就會變成這個。
    在這里插入圖片描述

37. 知道線程池中線程復用原理嗎?

  • 線程池將線程和任務進行解耦,線程是線程,任務是任務,擺脫了之前通過 Thread 創(chuàng)建線程時的一個線程必須對應一個任務的限制。
  • 在線程池中,同一個線程可以從阻塞隊列中不斷獲取新任務來執(zhí)行,其核心原理在于線程池對 Thread 進行了封裝,并不是每次執(zhí)行任務都會調(diào)用 Thread.start() 來創(chuàng)建新線程,而是讓每個線程去執(zhí)行一個“循環(huán)任務”,在這個“循環(huán)任務”中不停的檢查是否有任務需要被執(zhí)行,如果有則直接執(zhí)行,也就是調(diào)用任務中的 run 方法,將 run 方法當成一個普通的方法執(zhí)行,通過這種方式將只使用固定的線程就將所有任務的 run 方法串聯(lián)起來。

38. 什么是死鎖?

  • 當線程 A 持有獨占鎖a,并嘗試去獲取獨占鎖 b 的同時,線程 B 持有獨占鎖 b,并嘗試獲取獨占鎖 a 的情況下,就會發(fā)生 AB 兩個線程由于互相持有對方需要的鎖,而發(fā)生的阻塞現(xiàn)象,我們稱為死鎖。

39. ThreadLocal 是什么?有哪些使用場景?

  • ThreadLocal 為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。

40. 說一下 synchronized 底層實現(xiàn)原理?

  • synchronized 是由一對 monitorenter/monitorexit 指令實現(xiàn)的,monitor 對象是同步的基本實現(xiàn)單元。在 Java 6 之前,monitor 的實現(xiàn)完全是依靠操作系統(tǒng)內(nèi)部的互斥鎖,因為需要進行用戶態(tài)到內(nèi)核態(tài)的切換,所以同步操作是一個無差別的重量級操作,性能也很低。但在 Java 6 的時候,Java 虛擬機 對此進行了大刀闊斧地改進,提供了三種不同的 monitor 實現(xiàn),也就是常說的三種不同的鎖:偏向鎖(Biased Locking)、輕量級鎖和重量級鎖,大大改進了其性能。

41. synchronized 和 volatile 的區(qū)別是什么?

  • volatile 是變量修飾符;synchronized 是修飾類、方法、代碼段。
  • volatile 僅能實現(xiàn)變量的修改可見性,不能保證原子性;而 synchronized 則可以保證變量的修改可見性和原子性。
  • volatile 不會造成線程的阻塞;synchronized 可能會造成線程的阻塞。

42. synchronized 和 Lock 有什么區(qū)別?

  • synchronized 可以給類、方法、代碼塊加鎖;而 lock 只能給代碼塊加鎖。
  • synchronized 不需要手動獲取鎖和釋放鎖,使用簡單,發(fā)生異常會自動釋放鎖,不會造成死鎖;而 lock 需要自己加鎖和釋放鎖,如果使用不當沒有 unLock()去釋放鎖就會造成死鎖。
  • 通過 Lock 可以知道有沒有成功獲取鎖,而 synchronized 卻無法辦到。

43. synchronized 和 ReentrantLock 區(qū)別是什么?

synchronized 早期的實現(xiàn)比較低效,對比 ReentrantLock,大多數(shù)場景性能都相差較大,但是在 Java 6 中對 synchronized 進行了非常多的改進。
主要區(qū)別如下:

  • ReentrantLock 使用起來比較靈活,但是必須有釋放鎖的配合動作;
  • ReentrantLock 必須手動獲取與釋放鎖,而 synchronized 不需要手動釋放和開啟鎖;
  • ReentrantLock 只適用于代碼塊鎖,而 synchronized 可用于修飾方法、代碼塊等。

44. 說一下 atomic 的原理?

  • Atomic包中的類基本的特性就是在多線程環(huán)境下,當有多個線程同時對單個(包括基本類型及引用類型)變量進行操作時,具有排他性,即當多個線程同時對該變量的值進行更新時,僅有一個線程能成功,而未成功的線程可以向自旋鎖一樣,繼續(xù)嘗試,一直等到執(zhí)行成功。

45. 什么是反射?

  • 反射是在運行狀態(tài)中,對于任意一個類,都能夠知道這個類的所有屬性和方法;對于任意一個對象,都能夠調(diào)用它的任意一個方法和屬性;這種動態(tài)獲取的信息以及動態(tài)調(diào)用對象的方法的功能稱為 Java 語言的反射機制。

46. 創(chuàng)建對象有幾種方式

  • 4種
  • new 關(guān)鍵字
Example example = new Example();
  • 反射 這是我們運用反射創(chuàng)建對象時最常用的方法。Class類的newInstance使用的是類的public的無參構(gòu)造器。因此也就是說使用此方法創(chuàng)建對象的前提是必須有public的無參構(gòu)造器才行,否則報錯
// 1.
Example example = Example.class.newInstance();
// 2.Constructor.newInstance
// 本方法和Class類的newInstance方法很像,但是比它強大很多。 java.lang.relect.Constructor類里也有一個newInstance方法可以
//創(chuàng)建對象。我們可以通過這個newInstance方法調(diào)用有參數(shù)(不再必須是無參)的和私有的構(gòu)造函數(shù)(不再必須是public)。
Constructor<?>[] declaredConstructors = test.class.getDeclaredConstructors();
Constructor<?> noArgsConstructor = declaredConstructors[0];
noArgsConstructor.setAccessible(true); // 非public的構(gòu)造必須設置true才能用于創(chuàng)建實例
Object test = noArgsConstructor.newInstance();
  • 克隆 無論何時我們調(diào)用一個對象的clone方法,JVM就會創(chuàng)建一個新的對象,將前面的對象的內(nèi)容全部拷貝進去,用clone方法創(chuàng)建對象并不會調(diào)用任何構(gòu)造函數(shù)。 要使用clone方法,我們必須先實現(xiàn)Cloneable接口并復寫Object的clone方法。
public class Test {String b = "123";@Overridepublic Test clone() throws CloneNotSupportedException {return (Test) super.clone();}public Test() {Log.d("TAGGG", "print: init ");}
}public class Main {public static void main(String[] args) throws Exception {Test test= new Test();Object clone = Test.clone();System.out.println(test);System.out.println(clone);System.out.println(test == clone); //false}
}
  • 反序列化 當我們序列化和反序列化一個對象,JVM會給我們創(chuàng)建一個單獨的對象,在反序列化時,JVM創(chuàng)建對象并不會調(diào)用任何構(gòu)造函數(shù)。為了反序列化一個對象,我們需要讓我們的類實現(xiàn)Serializable接口。
public class Main {public static void main(String[] args) throws Exception {Test test= new Test();byte[] bytes = SerializationUtils.serialize(test);// 字節(jié)數(shù)組:可以來自網(wǎng)絡、可以來自文件(本處直接本地模擬)Object deserTest = SerializationUtils.deserialize(bytes);System.out.println(test);System.out.println(deserTest);System.out.println(test == deserTest);}
}

47. 使用過哪些設計模式?

  • 設計模式可分為三大類
    • 創(chuàng)建型模式——對象實例化的模式,創(chuàng)建型模式用于解耦對象的實例化過程。
      1. 單例模式:某個類只能有一個實例,提供一個全局的訪問點。
      2. 工廠方法模式:一個工廠類根據(jù)傳入的參量決定創(chuàng)建出哪一種產(chǎn)品類的實例。
      3. 抽象工廠模式:創(chuàng)建相關(guān)或依賴對象的家族,而無需明確指定具體類。
      4. 建造者模式:封裝一個復雜對象的創(chuàng)建過程,并可以按步驟構(gòu)造。
      5. 原型模式:通過復制現(xiàn)有的實例來創(chuàng)建新的實例。
    • 結(jié)構(gòu)型模式——把類或?qū)ο蠼Y(jié)合在一起形成一個更大的結(jié)構(gòu)。
      1. 裝飾器模式:動態(tài)的給對象添加新的功能。
      2. 代理模式:為其它對象提供一個代理以便控制這個對象的訪問。
      3. 橋接模式:將抽象部分和它的實現(xiàn)部分分離,使它們都可以獨立的變化。
      4. 適配器模式:將一個類的方法接口轉(zhuǎn)換成客戶希望的另一個接口。
      5. 組合模式:將對象組合成樹形結(jié)構(gòu)以表示“部分-整體”的層次結(jié)構(gòu)。
      6. 外觀模式:對外提供一個統(tǒng)一的方法,來訪問子系統(tǒng)中的一群接口。
      7. 享元模式:通過共享技術(shù)來有效的支持大量細粒度的對象。
    • 行為型模式
      1. 策略模式:定義一系列算法,把他們封裝起來,并且使它們可以相互替換。
      2. 模板方法模式:定義一個算法結(jié)構(gòu),而將一些步驟延遲到子類實現(xiàn)。
      3. 命令模式:將命令請求封裝為一個對象,使得可以用不同的請求來進行參數(shù)化。
      4. 迭代器模式:一種遍歷訪問聚合對象中各個元素的方法,不暴露該對象的內(nèi)部結(jié)構(gòu)。
      5. 察者模式:對象間的一對多的依賴關(guān)系。
      6. 仲裁者模式:用一個中介對象來封裝一系列的對象交互。
      7. 備忘錄模式:在不破壞封裝的前提下,保持對象的內(nèi)部狀態(tài)。
      8. 解釋器模式:給定一個語言,定義它的文法的一種表示,并定義一個解釋器。
      9. 狀態(tài)模式:允許一個對象在其對象內(nèi)部狀態(tài)改變時改變它的行為。
      10. 責任鏈模式:將請求的發(fā)送者和接收者解耦,使的多個對象都有處理這個請求的機會。
      11. 訪問者模式:不改變數(shù)據(jù)結(jié)構(gòu)的前提下,增加作用于一組對象元素的新功能。

48. 線程間如何通信?

  • 使用共享變量:多個線程可以通過共享變量來進行通信。通過對共享變量的讀寫操作,一個線程可以向另一個線程傳遞信息。
  • 使用wait()和notify()方法:線程可以通過調(diào)用wait()方法來等待某個條件的滿足,而其他線程可以通過調(diào)用notify()方法來通知等待的線程條件已經(jīng)滿足。
  • 使用Lock和Condition:Java并發(fā)包中的Lock和Condition接口提供了一種更靈活的線程通信機制。通過Lock接口的newCondition()方法可以獲得一個Condition對象,線程可以通過調(diào)用Condition對象的await()方法等待某個條件的滿足,而其他線程可以通過調(diào)用Condition對象的signal()或signalAll()方法來通知等待的線程條件已經(jīng)滿足。
  • 使用管道(PipedInputStream和PipedOutputStream):管道是一種特殊的流,可以用于在兩個線程之間傳遞數(shù)據(jù)。一個線程可以將數(shù)據(jù)寫入管道的輸出流,而另一個線程可以從管道的輸入流中讀取數(shù)據(jù)。
  • 使用阻塞隊列:Java并發(fā)包中的阻塞隊列(BlockingQueue)提供了一種線程安全的隊列實現(xiàn),可以用于在多個線程之間傳遞數(shù)據(jù)。一個線程可以將數(shù)據(jù)放入隊列中,而另一個線程可以從隊列中取出數(shù)據(jù)。
  • 使用信號量(Semaphore):信號量是一種計數(shù)器,用于控制同時訪問某個資源的線程數(shù)。線程可以通過調(diào)用信號量的acquire()方法獲取一個許可,從而允許同時訪問資源的線程數(shù)減少;線程可以通過調(diào)用信號量的release()方法釋放一個許可,從而允許同時訪問資源的線程數(shù)增加。
  • 使用CountDownLatch:CountDownLatch是一種同步工具類,可以用于控制一個或多個線程等待其他線程執(zhí)行完畢后再繼續(xù)執(zhí)行。一個線程可以通過調(diào)用CountDownLatch的await()方法等待其他線程執(zhí)行完畢,而其他線程可以通過調(diào)用CountDownLatch的countDown()方法告知自己已經(jīng)執(zhí)行完畢。
  • 使用CyclicBarrier:CyclicBarrier是一種同步工具類,可以用于控制多個線程在某個屏障處等待,直到所有線程都到達屏障后才繼續(xù)執(zhí)行。每個線程可以通過調(diào)用CyclicBarrier的await()方法等待其他線程到達屏障,而當所有線程都到達屏障后,屏障會自動打開,所有線程可以繼續(xù)執(zhí)行。

JVM相關(guān)

1. 簡單介紹下jvm虛擬機模型

分為三個部分

  • 類加載子系統(tǒng)(Class Loader Sub System)
  • 運行時數(shù)據(jù)區(qū)(Runtime Data Area)
  • 執(zhí)行引擎、本地方法接口(本地方法庫)(Execution Engine)
    在這里插入圖片描述

2. 類加載器子系統(tǒng)

Java虛擬機把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進行校驗、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這個過程被稱作虛擬機的類加載機制。

  • 類加載流程
    類從被加載到虛擬機內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期包括:加載,驗證,準備,解析,初始化,使用,卸載這7個階段,其中其中驗證、準備、解析3個部分統(tǒng)稱為連接。JVM沒有規(guī)定類加載的時機,但卻嚴格規(guī)定了五種情況下必須立即對類進行初始化,加載自然要在此之前。

    • 運行JVM必須指定一個含有main方法的主類,虛擬機會先初始化這個類。
    • 遇到new、getstatic、putstatic、invokestatic這四條指令時,如果類沒有被初始化,則首先對類進行初始化。
    • 使用java.lang.reflect包的方法對類進行反射調(diào)用時,若類沒有進行初始化,則觸發(fā)其初始化。
    • 當初始化一個類時假如該類的父類沒有進行初始化,首先觸發(fā)其父類的初始化。
    • 當使用Jdk1.7的動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結(jié)果REF_getstatic、 - REF_putstatic、REF_inokestatic的方法句柄,并且這個方法句柄所對應的類沒有進行初始化時,觸發(fā)該類初始化。
  • 1、加載
    在加載的過程中,虛擬機會完成以下三件事情:

    • 通過一個類的全限定名加載該類對應的二進制字節(jié)流。
    • 將字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。
    • 在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)各個類訪問該類的入口。
  • 2、驗證
    這一步的目的是為了確保class文件的字節(jié)流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。具體驗證的東西如下:

    • 文件格式驗證:這里驗證的時字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當前版本的虛擬機處理,例如:是否以 0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理范圍之內(nèi)、常量池中的常量是否有不被支持的類型。
    • 元數(shù)據(jù)的驗證:就是對字節(jié)碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規(guī)范的要求,例如:這個類是否有父類,除了 java.lang.Object之外。
    • 字節(jié)碼校驗:字節(jié)碼驗證也是驗證過程中最為復雜的一個過程。它試圖通過對字節(jié)碼流的分析,判斷字節(jié)碼是否可以被正確地執(zhí)行。比如:① 在字節(jié)碼的執(zhí)行過程中,是否會跳轉(zhuǎn)到一條不存在的指令。② 函數(shù)的調(diào)用是否傳遞了正確類型的參數(shù)。③ 變量的賦值是不是給了正確的數(shù)據(jù)類型等。
    • 符號引用驗證:虛擬機在將符號引用轉(zhuǎn)化為直接引用,驗證符號引用全限定名代表的類是否能夠找到,對應的域和方法是否能找到,訪問權(quán)限是否合法,如果一個需要使用類無法在系統(tǒng)中找到,則會拋出NoClassDefFoundError,如果一個 方法無法被找到,則會拋出NoSuchMethodError;這個轉(zhuǎn)化動作將在連接的第三個階段-解析階段中發(fā)生。
  • 3、準備
    為類變量(static修飾的變量)分配內(nèi)存并且設置該類變量的默認初始值,即零值,初始化階段才會設置代碼中的初始值
    這里不包含用final修飾的static,因為final在編譯的時候就會分配了,準備階段會顯示初始化
    這里不會為實例變量分配初始化,類變量會分配在方法區(qū),而實例變量是會隨著對象一起分配給Java堆中。

  • 4、解析
    解析階段是虛擬機將常量池內(nèi)的符號引用(類、變量、方法等的描述符 [名稱])替換為直接引用(直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄 [地址])的過程,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點限定符7類符號引用進行。

  • 5、初始化
    初始化階段編譯器會將類文件聲明的靜態(tài)賦值變量和靜態(tài)代碼塊合并生成方法并進行調(diào)用。

    • 初始化階段就是執(zhí)行類構(gòu)造器方法的過程,這個方法不需要定義,只需要類中有靜態(tài)的屬性或者代碼塊即可,javac編 譯器自動收集所有類變量的賦值動作和靜態(tài)代碼塊中的語句合并而來
    • 構(gòu)造器方法中指令按照源文件出現(xiàn)的順序執(zhí)行
    • 如果該類有父類,jvm會保證子類的在執(zhí)行前,執(zhí)行父類的
    • 虛擬機必須保證一個類的方法在多線程情況下被加鎖,類只需要被加載一次
  • 類加載器分類
    JVM層面支持兩種類加載器:啟動類加載器自定義類加載器,啟動類加載器由C++編寫,屬于虛擬機自身的一部分;繼承自java.lang.ClassLoader的類加載器都屬于自定義類加載器,由Java編寫。邏輯上我們可以根據(jù)各加載器的不同功能繼續(xù)劃分為:擴展類加載器、應用程序類加載器自定義類加載器。

    • 1、啟動類加載器
      • 由C/C++語言實現(xiàn),嵌套在JVM內(nèi)部
      • 負責加載Java的核心庫(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路徑下的內(nèi)容),用于提供JVM自身需要的類
      • 沒有父加載器,加載擴展類和應用程序類加載器,并作為他們的父類加載器
      • 出于安全考慮,Bootstrap啟動類加載器只加載包名為java、javax、sun等開頭的類
    • 2、擴展類加載器
      • 由Java語言實現(xiàn),派生于ClassLoader類
      • 負責加載java.ext.dirs系統(tǒng)屬性所指定目錄中的類庫,或JAVA_HOME/jre/lib/ext目錄(擴展目錄)下的類庫,如果用戶創(chuàng)建的jar放在此目錄下,也會自動由擴展類加載器加載
      • 作為類(在rt.jar中)被啟動類加載器加載,父類加載器為啟動類加載器
    • 3、應用程序類加載器
      • 由Java語言實現(xiàn),派生于ClassLoader類
      • 負責加載環(huán)境變量classpath或系統(tǒng)屬性java.class.path指定路徑下的類庫
      • 作為類被擴展類加載器加載,父類加載器為擴展類加載器
      • 該類加載是程序中默認的類加載器,一般來說,Java應用的類都是由它來完成加載
      • 通過ClassLoader.getSystemClassLoader()方法可以獲取到該類加載器,所以有些場合中也稱它為“系統(tǒng)類加載器”
    • 4、自定義類加載器
      在Java的日常應用程序開發(fā)中,類的加載幾乎是由上述3種類加載器相互配合執(zhí)行的,在必要時,我們還可以自定義類加載器,來定制類的加載方式。自定義類加載器作用:
      • 隔離加載類(相同包名和類名的兩個類會沖突,引入自己定義類加載器可以規(guī)避沖突問題)
      • 修改類加載的方式
      • 擴展加載源(默認從jar包、war包等源加載,可以自定義自己的源)
      • 防止源碼泄漏(對編譯后的class字節(jié)碼進行加密,加載時用自定義的類加載器進行解密后使用)
  • 類加載器寫協(xié)作方式
    Java虛擬機對class文件采用的是按需加載的方式,也就是說當需要使用該類時才會將它的class文件加載到內(nèi)存生成Class對象,當觸發(fā)類加載時,JVM并不知道當前類具體由哪個加載器加載,都是先給到默認類加載器(應用程序類加載器),默認類加載器怎么分配到具體的加載器呢,這邊使用了一種叫雙親委派模型的加載機制。

  • 1、雙親委派模型
    雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到最頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去完成加載。舉例如下:

    • 當AppClassLoader加載一個class時,它首先不會自己去嘗試加載這個類,而是把類加載請求委派給父類加載器ExtClassLoader去完成。
    • 當ExtClassLoader加載一個class時,它首先也不會自己去嘗試加載這個類,而是把類加載請求委派給BootStrapClassLoader去完成。
    • 如果BootStrapClassLoader加載失敗(例如在$JAVA_HOME/jre/lib里未查找到該class),會使用ExtClassLoader來嘗試加載;
      若ExtClassLoader也加載失敗,則會使用AppClassLoader來加載,如果AppClassLoader也加載失敗,則會報出異常ClassNotFoundException。
  • 2、全盤負責
    當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入。

  • 3、緩存機制
    緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存區(qū)尋找該Class,只有緩存區(qū)不存在,系統(tǒng)才會讀取該類對應的二進制數(shù)據(jù),并將其轉(zhuǎn)換成Class對象,存入緩存區(qū)。這就是為什么修改了Class后,必須重啟JVM,程序的修改才會生效。

使用雙親委派模型來組織類加載器之間的關(guān)系,有一個顯而易見的好處就是Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系。例如類java.lang.Object,它存放在rt.jar之中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器進行加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個稱為java.lang.Object的類,并放在程序的ClassPath中,那系統(tǒng)中將會出現(xiàn)多個不同的Object類,Java類型體系中最基礎(chǔ)的行為也就無法保證,應用程序也將會變得一片混亂。

3.運行時數(shù)據(jù)區(qū)

按照是否線程私有可分為

  • 線程共有:方法區(qū)
  • 線程私有 java虛擬機棧本地方法棧程序計數(shù)器

1.方法區(qū)
在這里插入圖片描述

方法區(qū),也稱非堆(Non-Heap),是一個被線程共享的內(nèi)存區(qū)域。其中主要存儲類的類型信息方法信息域信息JIT代碼緩存運行時常量池等。

    1. 方法區(qū)是各個線程共享的內(nèi)存區(qū)域,在虛擬機啟動時創(chuàng)建
    1. 雖然Java虛擬機規(guī)范把方法區(qū)描述為堆的一個邏輯部分,但是它卻又一個別名叫做Non-Heap(非堆),目的是與Java堆區(qū)分開來
    1. 用于存儲已被虛擬機加載的類信息、常量、即時編譯器編譯后的代碼等數(shù)據(jù)。
    1. 當方法區(qū)無法滿足內(nèi)存分配需求時,將拋出OutOfMemoryError異常
      在JDK7之前,習慣把方法區(qū)稱為永久代,而在JDK8之后,又取消了永久代,改用元空間代替。元空間的本質(zhì)與方法區(qū)類似,都是對JVM規(guī)范中方法區(qū)這一內(nèi)存區(qū)域的一種實現(xiàn)。不過元空間與永久代的最大區(qū)別就是:元空間不在虛擬機設置的內(nèi)存中,而是直接使用的本地內(nèi)存。所以元空間的大小并不受虛擬機本身的內(nèi)存限制,而是受制于計算機的直接內(nèi)存。

2. 堆 Java堆是Java虛擬機所管理的內(nèi)存最大的一塊區(qū)域,Java堆是線程共享的,在虛擬機啟動時創(chuàng)建。

  • 幾乎所有的對象實例都在這里分配內(nèi)存。

  • 字符串常量池(String Table),靜態(tài)變量也在這里分配內(nèi)存。

  • Java堆是垃圾收集器管理的內(nèi)存區(qū)域,有些資料稱為GC堆,當對象不再使用了,被當做垃圾回收掉后,這些為對象分配的內(nèi)存又重新回到堆內(nèi)存中。

  • Java堆在邏輯上應該認為是連續(xù)的,但是在具體的物理實現(xiàn)上,可以是不連續(xù)的。

  • Java堆可以是固定大小的,也可以是可擴展的。現(xiàn)在主流Java虛擬機都是可擴展的。
    -Xmx 最大堆內(nèi)存
    -Xms 最小堆內(nèi)存

  • 如果Java堆沒有足夠的內(nèi)存給分配實例,并且也無法繼續(xù)擴展,則拋出 OutOfMemoryError 異常。

    • 1.堆內(nèi)存結(jié)構(gòu)
      • 堆內(nèi)存從結(jié)構(gòu)上來說分為年輕代(YoungGen)和老年代(OldGen)兩部分;
      • 年輕代(YoungGen)又可以分為生成區(qū)(Eden)和幸存者區(qū)(Survivor)兩部分;
      • 幸存者區(qū)(Survivor)又可細分為 S0區(qū)(from space)和 S1區(qū) (to space)兩部分;
      • Eden 區(qū)占大容量,Survivor 兩個區(qū)占小容量,默認比例是 8:1:1;
      • 靜態(tài)變量和字符串常量池在年輕代與老年代之外單獨分配空間。
        在這里插入圖片描述

3.Java虛擬機棧 Java虛擬機棧(Java Virtual Machine Stack),早期也叫Java棧。每個線程在創(chuàng)建時都會創(chuàng)建一個虛擬機棧,其內(nèi)部保存一個個的棧幀(Stack Frame),對應著一次次的Java方法調(diào)用,是線程私有的,因此也是線程安全的。

  • Java虛擬機棧是線程私有的,其生命周期和線程相同。
  • 虛擬機棧描述的是Java方法執(zhí)行的線程內(nèi)存模型,每個方法被執(zhí)行,都會創(chuàng)建一個棧幀用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、參與方法的調(diào)用與返回等。
  • 每一個方法被調(diào)用到執(zhí)行完成的過程,就對應著一個棧幀在虛擬機棧中出入棧到出棧的過程
  • JVM 允許指定 Java 棧的初始大小以及最大、最小容量。
  • 1.棧幀
    • 定義:棧幀(Stack Frame)是用于支持虛擬機進行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu)。它是虛擬機運行時數(shù)據(jù)區(qū)中的 Java 虛擬機棧的棧元素。棧幀存儲了方法的局部變量表、操作數(shù)棧、動態(tài)鏈接方法返回地址等信息。
    • 棧幀初始化大小:在編譯程序代碼的時候,棧幀中需要多大的局部變量表內(nèi)存,多深的操作數(shù)棧都已經(jīng)完全確定了。 因此一個棧幀需要分配多少內(nèi)存,不會受到程序運行期變量數(shù)據(jù)的影響,而僅僅取決于具體的虛擬機實現(xiàn)。
    • 棧幀結(jié)構(gòu):在一個線程中,只有位于棧頂?shù)臈攀怯行У?#xff0c;稱為當前棧幀,與這個棧幀相關(guān)聯(lián)的方法稱為當前方法。每一個方法從調(diào)用開始至執(zhí)行完成的過程,都對應著一個棧幀在虛擬機里面從入棧到出棧的過程。
      -在這里插入圖片描述在這里插入圖片描述

4.程序計數(shù)器 程序計數(shù)器的英文全稱是Program Counter Register,又叫程序計數(shù)寄存器。Register的命名源于CPU的寄存器,寄存器存儲指令相關(guān)的現(xiàn)場信息。JVM中的PC寄存器是對 物理PC寄存器的一種抽象模擬。
在這里插入圖片描述

  • 程序計數(shù)器其實就是一個指針,它指向了我們程序中下一句需要執(zhí)行的指令,可以看做當前線程執(zhí)行的字節(jié)碼的行數(shù)指示器。
  • 不管是分支、循環(huán)、跳轉(zhuǎn)等代碼邏輯,字節(jié)碼解釋器在工作時就是改變程序計數(shù)器的值來決定下一條要執(zhí)行的字節(jié)碼。
  • 每個線程都有一個獨立的程序計數(shù)器,在任意一個確定的時刻,一個CPU內(nèi)核都只會執(zhí)行一條線程中的指令,CPU切換線程后是通過程序計數(shù)器來確定該執(zhí)行哪條指令。
  • 程序計數(shù)器占用內(nèi)存空間小到基本可以忽略不計,是唯一一個在虛擬機中沒有規(guī)定任何OutOfMemoryError 情況的區(qū)域。
  • 如果正在執(zhí)行的是Native方法,則這個計數(shù)器為空。

5.本地方法棧 本地方法棧與虛擬機棧所發(fā)揮的作用是非常相似的。只不過虛擬機棧為虛擬機執(zhí)行的Java方法(即字節(jié)碼)服務,本地方法棧為虛擬機執(zhí)行的本地方法(Native方法、C/C++ 實現(xiàn))服務。

  • 與虛擬機棧一樣,當棧深度溢出時,拋出 StackOverFlowError 異常。
  • 當棧擴展內(nèi)存不足時,拋出 OutOfMemoryError 異常。

4. 執(zhí)行引擎

負責執(zhí)行class文件中包含的字節(jié)碼指令;
在這里插入圖片描述
JVM的主要任務之一是負責裝載字節(jié)碼到其內(nèi)部(運行時數(shù)據(jù)區(qū)),但字節(jié)碼并不能夠直接運行在操作系統(tǒng)之上,因為字節(jié)碼指令并非等價于本地機器指令,它內(nèi)部包含的僅僅只 是一些能夠被 JVM 所識別的字節(jié)碼指令、符號表,以及其他輔助信息。

那么,如果想要讓一個 Java 程序運行起來,執(zhí)行引擎(Execution Engine) 的任務就是將字節(jié)碼指令解釋/編譯為對應平臺上的本地機器指令才可以。簡單來說,JVM 中的執(zhí)行引擎充當了將高級語言翻譯為機器語言的譯者。

1. 解釋器
當Java虛擬機啟動時會根據(jù)預定義的規(guī)范對字節(jié)碼采用逐行解釋的方式執(zhí)行,將每條字節(jié)碼文件中的內(nèi)容“翻譯”為對應平臺的本地機器指令然后執(zhí)行。

JVM 設計者們的初衷僅僅只是單純地為了滿足 Java 程序?qū)崿F(xiàn)跨平臺特性,因此避免采用靜態(tài)編譯的方式由高級語言直接生成本地機器指令,從而誕生了實現(xiàn)解釋器在運行時采用逐行解釋字節(jié)碼執(zhí)行程序的想法。
解釋器真正意義上所承擔的角色就是一個運行時“翻譯者”,將字節(jié)碼文件中的內(nèi)容“翻譯”為對應平臺的本地機器指令執(zhí)行,執(zhí)行效率低。

在Java的發(fā)展歷史里,一共有兩套解釋執(zhí)行器,即古老的字節(jié)碼解釋器、現(xiàn)在普遍使用的模板解釋器。字節(jié)碼解釋器在執(zhí)行時通過純軟件代碼模擬字節(jié)碼的執(zhí)行,效率非常低效。而模板解釋器將每一條字節(jié)碼和一個模板函數(shù)性關(guān)聯(lián),模板函數(shù)中直接產(chǎn)生這條字節(jié)碼執(zhí)行時的機器碼,從而很大程度上提高了解釋器的性能。

在Hotspot VM中,解釋器主要由Interpreter模塊和Code模塊構(gòu)成。

  • Interpreter模塊:實現(xiàn)了解釋器的核心功能。
  • Code模塊:用于管理Hotspot VM在與運行時生成的本地機器指令。

由于解釋器在設計和實現(xiàn)上非常簡單,因此除了 Java 語言之外,還有許多高級語言同樣也是基于解釋器執(zhí)行的,比如:Python、Perl、Ruby等。但就是因為多了中間這一“翻譯”過程,導致代碼執(zhí)行效率低下。

為了解決這個問題,JVM平臺支持一種叫做即時編譯的的技術(shù)。即時編譯的目的是為了避免函數(shù)被解釋執(zhí)行,而是將整個函數(shù)編譯成機器碼,每次函數(shù)執(zhí)行時,只執(zhí)行編譯后的機器碼即可,這種方式可以使執(zhí)行效率大幅度提升。

2. 即時(JIT)編譯器
就是虛擬機將Java字節(jié)碼一次性整體編譯成和本地機器平臺相關(guān)的機器語言,但并不是馬上執(zhí)行。JIT 編譯器將字節(jié)碼翻譯成本地機器指令后,就可以做一個緩存操作,存儲在方法區(qū) 的 JIT 代碼緩存中。JVM真正執(zhí)行程序時將直接從緩存中獲取本地指令去執(zhí)行,省去了解釋器的工作,提高了執(zhí)行效率高。

HotSpot VM 是目前市面上高性能虛擬機的代表作之一。它采用解釋器與及時編輯器并行的結(jié)構(gòu)。在 Java 虛擬機運行時,解釋器和即時編譯器能夠相互協(xié)作,各自取長補短,盡力去選擇最合適的方式來權(quán)衡編譯本地代碼的時間和直接解釋執(zhí)行代碼的時間。

JIT 編譯器執(zhí)行效率高為什么還需要解釋器?

  • 當程序啟動后,解釋器可以馬上發(fā)揮作用,響應速度快,省去編譯的時間,立即執(zhí)行。
  • 編譯器要想發(fā)揮作用,把代碼編譯成本地代碼,需要一定的執(zhí)行時間,但編譯為本地代碼后,執(zhí)行效率高。就需要采用解釋器與即時編譯器并存的架構(gòu)來換取 一個平衡點。
    是否需要啟動JIT編譯器將字節(jié)碼直接編譯為對應平臺的本地機器指令,則需要根據(jù)代碼被調(diào)用的執(zhí)行頻率而定。關(guān)于那些需要被編譯成本地代碼的字節(jié)碼,也被稱為熱點代碼,JIT編譯器在運行時會對那些頻繁被調(diào)用的熱點代碼做出深度優(yōu)化,將其直接編譯成對應平臺的本地機器指令,以此提升Java程序的執(zhí)行性能。

一個被多次調(diào)用的方法,或者是一個方法體內(nèi)部循環(huán)次數(shù)較多的循環(huán)體,都可以被稱為熱點代碼。因此都可以通過JIT編譯器編譯成本地機器指令。由于這種編譯方式發(fā)生在方法執(zhí)行的過程中,因此也被稱為棧上替換,或者簡稱為OSR編譯。

一個方法究竟要被調(diào)用多少次,或者一個循環(huán)體究竟需要執(zhí)行多少次循環(huán)才可以達到這個標準,必然需要一個明確的閾值。JIT編譯器才會將這些熱點代碼編譯成本地機器碼執(zhí)行。

5. 了解GC嗎

GC是JVM中的垃圾回收機制。主要作用于Java堆區(qū),用于將不再使用的對象回收,釋放內(nèi)存。簡單的說垃圾就是內(nèi)存中不再使用的對象,所謂使用中的對象(已引用對象),指的是程序中有指針指向的對象;而不再使用的對象(未引用對象),則沒有被任何指針指向。如果這些不再使用的對象不被清除掉,我們內(nèi)存里面的對象會越來越多,而可使用的內(nèi)存空間會越來越少,最后導致無空間可用。
垃圾回收的基本步驟分兩步:

  • 查找內(nèi)存中不再使用的對象(GC判斷策略)
  • 釋放這些對象占用的內(nèi)存(GC收集算法)

1.對象存活判斷 即內(nèi)存中不再使用的對象,判斷對象存活一般有兩種方式:引用計數(shù)算法和可達性分析法

  • 1. 引用計數(shù)算法 給對象添加一個引用計數(shù)器,每當有一個地方引用該對象時,計數(shù)器+1,當引用失效時,計數(shù)器-1,任何時候當計數(shù)器為0的時候,該對象不再被引用。

    • 優(yōu)點:引用計數(shù)器這個方法實現(xiàn)簡單,判定效率也高,回收沒有延遲性。
    • 缺點無法檢測出循環(huán)引用。 如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數(shù)永遠不可能為0,Java的垃圾收集器沒有使用這類算法。
  • 2. 可達性分析算法 可達性分析算法是目前主流的虛擬機都采用的算法,程序把所有的引用關(guān)系看作一張圖,從所有的GC Roots節(jié)點開始,尋找該節(jié)點所引用的節(jié)點,找到這個節(jié)點以后,繼續(xù)尋找這個節(jié)點的引用節(jié)點,當所有的引用節(jié)點尋找完畢之后,剩余的節(jié)點則被認為是沒有被引用到的節(jié)點,即無用的節(jié)點,無用的節(jié)點將會被判定為是可回收的對象。
    在這里插入圖片描述

在Java語言中,可作為GC Roots的對象包括下面幾種:
- 虛擬機棧中引用的對象(局部變量);
- 方法區(qū)中類靜態(tài)屬性引用的對象;
- 方法區(qū)中常量引用的對象;
- 本地方法棧中JNI(Native方法)引用的對象
- 所有被同步鎖持有的對象;
- 虛擬機的內(nèi)部引用如類加載器、異常管理對象;
- 反映java虛擬機內(nèi)部情況的JMXBean、JVMTI中注冊的回調(diào)、本地代碼緩存等

2.垃圾回收算法

  1. 標記-清除算法 標記-清除算法的基本思想就跟它的名字一樣,分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成后統(tǒng)一回收所有被標記的對象

    1. 標記階段 標記的過程其實就是前面介紹的可達性分析算法的過程,遍歷所有的 GC Roots 對象,對從 GCRoots 對象可達的對象都打上一個標識,一般是在對象的 header 中,將其記錄為可達對象;

    2. 清除階段清除的過程是對堆內(nèi)存進行遍歷,如果發(fā)現(xiàn)某個對象沒有被標記為可達對象(通過讀取對象header 信息),則將其回收。
      在這里插入圖片描述

    標記-清除算法缺點

    1. 效率問題
      標記和清除兩個階段的效率都不高,因為這兩個階段都需要遍歷內(nèi)存中的對象,很多時候內(nèi)存中的對象實例數(shù)量是非常龐大的,這無疑很耗費時間,而且 GC 時需要停止應用程序,這會導致非常差的用戶體驗。
    2. 空間問題
      標記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片(從上圖可以看出),內(nèi)存空間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾回收動作。

2. 復制算法

復制算法是將可用內(nèi)存按容量劃分為大小相等的兩塊,每次使用其中的一塊。當這一塊的內(nèi)存用完了,就將還存活的對象復制到另一塊內(nèi)存上,然后把這一塊內(nèi)存所有的對象一次性清理掉。
在這里插入圖片描述
復制算法每次都是對整個半?yún)^(qū)進行內(nèi)存回收,這樣就減少了標記對象遍歷的時間,在清除使用區(qū)域?qū)ο髸r,不用進行遍歷,直接清空整個區(qū)域內(nèi)存,而且在將存活對象復制到保留區(qū)域時也是按地址順序存儲的,這樣就解決了內(nèi)存碎片的問題,在分配對象內(nèi)存時不用考慮內(nèi)存碎片等復雜問題,只需要按順序分配內(nèi)存即可。

復制算法優(yōu)點

  • 復制算法簡單高效,優(yōu)化了標記清除算法的效率低、內(nèi)存碎片多問題

復制算法缺點

  • 將內(nèi)存縮小為原來的一半,浪費了一半的內(nèi)存空間,代價太高;
  • 如果對象的存活率很高,極端一點的情況假設對象存活率為 100%,那么我們需要將所有存活的對象復制一遍,耗費的時間代價也是不可忽視的。

3. 標記-整理算法

標記-整理算法算法與標記-清除算法很像,事實上,標記-整理算法的標記過程任然與標記-清除算法一樣,但后續(xù)步驟不是直接對可回收對象進行回收,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊線以外的內(nèi)存。
在這里插入圖片描述
可以看到,回收后可回收對象被清理掉了,存活的對象按規(guī)則排列存放在內(nèi)存中。這樣一來,當我們給新對象分配內(nèi)存時,JVM只需要持有內(nèi)存的起始地址即可。標記/整理算法彌補了標記/清除算法存在內(nèi)存碎片的問題消除了復制算法內(nèi)存減半的高額代價,可謂一舉兩得。
標記-整理缺點

  • 效率不高:不僅要標記存活對象,還要整理所有存活對象的引用地址,在效率上不如復制算法。

4. 分代收集算法

前文介紹JVM堆內(nèi)存時已經(jīng)說過了分代概念和對象在分代中的轉(zhuǎn)移,垃圾回收伴隨了對象的轉(zhuǎn)移,其中新生代的回收算法以復制算法為主,老年代的回收算法以標記-清除以及標記-整理為主。

5. 方法區(qū)的垃圾回收
方法區(qū)主要回收的內(nèi)容有:廢棄常量和無用的類。Full GC(Major GC)的時候會觸發(fā)方法區(qū)的垃圾回收。

  • 廢棄常量
    通過可達性分析算法確定的可回收常量
  • 無用類
    對于無用的類的判斷則需要同時滿足下面3個條件:
    (1)該類所有的實例都已經(jīng)被回收,也就是Java堆中不存在該類的任何實例;
    (2)加載該類的ClassLoader已經(jīng)被回收;
    (3)該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

6. 了解JMM嗎

  • JMM 是Java內(nèi)存模型( Java Memory Model),簡稱JMM。它本身只是一個抽象的概念,并不真實存在,它描述的是一種規(guī)則或規(guī)范,是和多線程相關(guān)的一組規(guī)范。通過這組規(guī)范,定義了程序中對各個變量(包括實例字段,靜態(tài)字段和構(gòu)成數(shù)組對象的元素)的訪問方式。需要每個JVM 的實現(xiàn)都要遵守這樣的規(guī)范,有了JMM規(guī)范的保障,并發(fā)程序運行在不同的虛擬機上時,得到的程序結(jié)果才是安全可靠可信賴的。如果沒有JMM 內(nèi)存模型來規(guī)范,就可能會出現(xiàn),經(jīng)過不同 JVM 翻譯之后,運行的結(jié)果不相同也不正確的情況。
  • JMM 抽象出 主存儲器(Main Memory)工作存儲器(Working Memory) 兩種。
  • 主存儲器是實例對象所在的區(qū)域,所有的實例都存在于主存儲器內(nèi)。比如,實例所擁有的字段即位于主存儲器內(nèi),主存儲器是所有的線程所共享的。
  • 工作存儲器是線程所擁有的作業(yè)區(qū),每個線程都有其專用的工作存儲器。工作存儲器存有主存儲器中必要部分的拷貝,稱之為工作拷貝(Working Copy)。
    所以,線程無法直接對主內(nèi)存進行操作,此外,線程A想要和線程B通信,只能通過主存進行。
  • JMM的三大特性:原子性、可見性、有序性。
    • 一個或多個操作,要么全部執(zhí)行,要么全部不執(zhí)行(執(zhí)行的過程中是不會被任何因素打斷的)。
    • 只要有一個線程對共享變量的值做了修改,其他線程都將馬上收到通知,立即獲得最新值。
    • 有序性可以總結(jié)為:在本線程內(nèi)觀察,所有的操作都是有序的;而在一個線程內(nèi)觀察另一個線程,所有操作都是無序的。前半句指 as-if-serial 語義:線程內(nèi)似表現(xiàn)為串行,后半句是指:“指令重排序現(xiàn)象”和“工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象”。處理器為了提高程序的運行效率,提高并行效率,可能會對代碼進行優(yōu)化。編譯器認為,重排序后的代碼執(zhí)行效率更優(yōu)。這樣一來,代碼的執(zhí)行順序就未必是編寫代碼時候的順序了,在多線程的情況下就可能會出錯。
      ? 在代碼順序結(jié)構(gòu)中,我們可以直觀的指定代碼的執(zhí)行順序, 即從上到下按序執(zhí)行。但編譯器和CPU處理器會根據(jù)自己的決策,對代碼的執(zhí)行順序進行重新排序,優(yōu)化指令的執(zhí)行順序,提升程序的性能和執(zhí)行速度,使語句執(zhí)行順序發(fā)生改變,出現(xiàn)重排序,但最終結(jié)果看起來沒什么變化(在單線程情況下)。
      ? 有序性問題 指的是在多線程的環(huán)境下,由于執(zhí)行語句重排序后,重排序的這一部分沒有一起執(zhí)行完,就切換到了其它線程,導致計算結(jié)果與預期不符的問題。這就是編譯器的編譯優(yōu)化給并發(fā)編程帶來的程序有序性問題。
      Java 語言提供了 volatile 和 synchronized 兩個關(guān)鍵字來保證線程之間操作的有序性,volatile 是因為其本身包含“禁止指令重排序”的語義,synchronized 是由“一個變量在同一個時刻只允許一條線程對其進行 lock 操作”這條規(guī)則獲得的,此規(guī)則決定了持有同一個對象鎖的兩個同步塊只能串行進入。

7. 類的加載過程,Person person = new Person();為例進行說明。

  1. 因為new用到了Person.class,所以會先找到Person.class文件,并加載到內(nèi)存中;
  2. 執(zhí)行該類中的static代碼塊,如果有的話,給Person.class類進行初始化;
  3. 在堆內(nèi)存中開辟空間分配內(nèi)存地址;
  4. 在堆內(nèi)存中建立對象的特有屬性,并進行默認初始化;
  5. 對屬性進行顯示初始化;
  6. 對對象進行構(gòu)造代碼塊初始化;
  7. 對對象進行與之對應的構(gòu)造函數(shù)進行初始化;
  8. 將內(nèi)存地址付給棧內(nèi)存中的person變量

kotlin

1. Kotlin如何實現(xiàn)空安全的?

  • Kotlin 將變量劃分為可空和不可空,通過查看字節(jié)碼可知,聲明不可空的變量會加 @NonNull注解,會告訴編譯器檢查變量是否可空。聲明可空的變量會加 @Nullable注解。
  • Kotlin 提供了空安全操作符 ?相當于實現(xiàn)了非空判斷,當對象不為空時才執(zhí)行操作,否則不執(zhí)行。保證了空安全
//場景1,m1方法接收一個不可能為null的字符串
//在其方法體中我們獲取了傳入字符串的長度
fun m1(str: String) {str.length
}
//場景2,m2方法接收一個可能為null的字符串
//在其方法體中我們采用了安全調(diào)用操作符 ?. 來獲取傳入字符串的長度
fun m2(str: String?) {str?.length
}
//場景3,m3方法接收一個可能為null的字符串
//在其方法體中我們采用了 !!  來獲取傳入字符串的長度
fun m3(str: String?) {str!!.length
}public final static m1(Ljava/lang/String;)V@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0L0ALOAD 0LDC "str"INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)VL1LINENUMBER 6 L1ALOAD 0INVOKEVIRTUAL java/lang/String.length ()IPOPL2LINENUMBER 7 L2RETURNL3LOCALVARIABLE str Ljava/lang/String; L0 L3 0MAXSTACK = 2MAXLOCALS = 1// access flags 0x19public final static m2(Ljava/lang/String;)V@Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0L0LINENUMBER 10 L0ALOAD 0DUPIFNULL L1INVOKEVIRTUAL java/lang/String.length ()IPOPGOTO L2L1POPL2L3LINENUMBER 11 L3RETURNL4LOCALVARIABLE str Ljava/lang/String; L0 L4 0MAXSTACK = 2MAXLOCALS = 1public final static m3(Ljava/lang/String;)V@Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0L0LINENUMBER 15 L0ALOAD 0DUPIFNONNULL L1INVOKESTATIC kotlin/jvm/internal/Intrinsics.throwNpe ()VL1INVOKEVIRTUAL java/lang/String.length ()IPOPL2LINENUMBER 16 L2RETURNL3LOCALVARIABLE str Ljava/lang/String; L0 L3 0MAXSTACK = 3MAXLOCALS = 1

2. 談談你對協(xié)程的理解

協(xié)程可以看做是官方封裝的輕量級線程框架。線程是由系統(tǒng)調(diào)度的,線程切換或線程阻塞的開銷都比較大。而協(xié)程依賴于線程,但是協(xié)程掛起時不需要阻塞線程,幾乎是無代價的,協(xié)程是由開發(fā)者控制的。所以協(xié)程也像用戶態(tài)的線程,非常輕量級,一個線程中可以創(chuàng)建任意個協(xié)程。

  • 協(xié)程與線程有什么區(qū)別:
    ·Kotlin協(xié)程,不是操作系統(tǒng)級別的概念,無需操作系統(tǒng)支持,線程是操作系統(tǒng)級別的概念,我們開發(fā)者通過編程語言(Thread,java)創(chuàng)建的線程,本質(zhì)還是操作系統(tǒng)內(nèi)核線程的映射。
  • Kotlin協(xié)程,是用戶態(tài)的(userleve),內(nèi)核對協(xié)程無感知;一般情況下,我們說的線程,都是內(nèi)核線程,線程之間的切換,調(diào)
    度,都由操作系統(tǒng)負責。
  • Kotlin協(xié)程,是協(xié)作式的,由開發(fā)者管理,不需要操作系統(tǒng)進行調(diào)度和切換,也沒有搶占式的消耗,因比它更加高效;線程,是
    搶占式的,它們之間能共享內(nèi)存資源。
  • Kotlin協(xié)程,它底層基于狀態(tài)機實現(xiàn),多協(xié)程之間共用一個實例,資源開銷極小,因比它更加輕量;線程會消耗操作系統(tǒng)資源。
  • Kotlin協(xié)程,本質(zhì)還是運行于線程之上,它通過協(xié)程調(diào)度器,可以運行到不同的線程上
    優(yōu)點:
  • 輕量和高效:協(xié)程可以在一個線程中開啟1000個協(xié)程,也不會有什么影響。
  • 簡單好用:其實輕量和高效并不是協(xié)程的核心競爭力,最主要的還是簡化異步并發(fā)任務,代碼中可以已同步的方式替換異步,去除java中回調(diào)地獄問題。

3. 了解密封類(Sealed Classes)嗎

可以理解成是Enum枚舉類的加強版

  • Sealed class(密封類) 是一個有特定數(shù)量子類的類,看上去和枚舉有點類似,所不同的是,在枚舉中,我們每個類型只有一個對象(實例);而在密封類中,同一個類可以擁有幾個對象。
  • Sealed class(密封類)的所有子類都必須與密封類在同一文件中
  • Sealed class(密封類)的子類的子類可以定義在任何地方,并不需要和密封類定義在同一個文件中
  • Sealed class(密封類)沒有構(gòu)造函數(shù),不可以直接實例化,只能實例化內(nèi)部的子類
sealed class SealedClass{class SealedClass1():SealedClass()class SealedClass2():SealedClass()fun hello(){println("Hello World ... ")}
}
fun main(args:Array<String>){var sc:SealedClass = SealedClass()//這里直接編譯報錯
}
fun main(args:Array<String>){var sc:SealedClass = SealedClass.SealedClass1()//只能通過密封類內(nèi)部的子類實例化對象,這時就可以執(zhí)行里面的方法了sc.hello()
}

使用場景:與when表達式搭配

// Result.kt
sealed class Result<out T : Any> {data class Success<out T : Any>(val data: T) : Result<T>()data class Error(val exception: Exception) : Result<Nothing>()
}when(result) {is Result.Success -> { }is Result.Error -> { }}

但是如果有人為 Result 類添加了一個新的類型: InProgress:

sealed class Result<out T : Any> { data class Success<out T : Any>(val data: T) : Result<T>()data class Error(val exception: Exception) : Result<Nothing>()object InProgress : Result<Nothing>()
}

如果想要防止遺漏對新類型的處理,并不一定需要依賴我們自己去記憶或者使用 IDE 的搜索功能確認新添加的類型。使用 when 語句處理密封類時,如果沒有覆蓋所有情況,可以讓編譯器給我們一個錯誤提示。和 if 語句一樣,when 語句在作為表達式使用時,會通過編譯器報錯來強制要求必須覆蓋所有選項 (也就是說要窮舉):

val action = when(result) {is Result.Success -> { }is Result.Error -> { }
}

當表達式必須覆蓋所有選項時,添加 “is inProgress” 或者 “else” 分支。
如果想要在使用 when 語句時獲得相同的編譯器提示,可以添加下面的擴展屬性:

val <T> T.exhaustive: Tget() = this

這樣一來,只要給 when 語句添加 “.exhaustive”,如果有分支未被覆蓋,編譯器就會給出之前一樣的錯誤。

when(result){is Result.Success -> { }is Result.Error -> { }
}.exhaustive

IDE 自動補全
由于一個密封類的所有子類型都是已知的,所以 IDE 可以幫我們補全 when 語句下的所有分支:
在這里插入圖片描述
當涉及到一個層級復雜的密封類時,這個功能會顯得更加好用,因為 IDE 依然可以識別所有的分支:
在這里插入圖片描述

sealed class Result<out T : Any> {data class Success<out T : Any>(val data: T) : Result<T>()sealed class Error(val exception: Exception) : Result<Nothing>() {class RecoverableError(exception: Exception) : Error(exception)class NonRecoverableError(exception: Exception) : Error(exception)}object InProgress : Result<Nothing>()
}

3. Kotlin中@JvmOverloads 的作用

在Kotlin中@JvmOverloads注解的作用就是:在有默認參數(shù)值的方法中使用@JvmOverloadsi注解,則Kotlin就會暴露多個重載方法。如果沒有加注解@JvmOverloads則只有一個方法,kotlini調(diào)用的話如果沒有傳入的參數(shù)用的是默認值。

@JvmOverloads fun f(a: String, b: Int=0, c:String="abc"){
}
// 相當于Java三個方法 不加這個注解就只能當作第三個方法這唯一一種方法
void f(String a)
void f(String a, int b)
// 加不加注解,都會生成這個方法
void f(String a, int b, String c)

4.Kotlin實現(xiàn)單例的幾種方式

  • 餓漢式
//Java實現(xiàn)
public class SingletonDemo {private static SingletonDemo instance=new SingletonDemo();private SingletonDemo(){}public static SingletonDemo getInstance(){return instance;}
}
//Kotlin實現(xiàn)
object SingletonDemo
  • 懶漢式
//Java實現(xiàn)
public class SingletonDemo {private static SingletonDemo instance;private SingletonDemo(){}public static SingletonDemo getInstance(){if(instance==null){instance=new SingletonDemo();}return instance;}
}
//Kotlin實現(xiàn)
class SingletonDemo private constructor() {companion object {private var instance: SingletonDemo? = nullget() {if (field == null) {field = SingletonDemo()}return field}fun get(): SingletonDemo{//細心的小伙伴肯定發(fā)現(xiàn)了,這里不用getInstance作為為方法名,是因為在伴生對象聲明時,內(nèi)部已有g(shù)etInstance方法,所以只能取其他名字return instance!!}}
}線程安全的懶漢式//Java實現(xiàn)
public class SingletonDemo {private static SingletonDemo instance;private SingletonDemo(){}public static synchronized SingletonDemo getInstance(){//使用同步鎖if(instance==null){instance=new SingletonDemo();}return instance;}
}
//Kotlin實現(xiàn)
class SingletonDemo private constructor() {companion object {private var instance: SingletonDemo? = nullget() {if (field == null) {field = SingletonDemo()}return field}@Synchronizedfun get(): SingletonDemo{return instance!!}}}
  • 雙重校驗鎖式
//Java實現(xiàn)
public class SingletonDemo {private volatile static SingletonDemo instance;private SingletonDemo(){} public static SingletonDemo getInstance(){if(instance==null){synchronized (SingletonDemo.class){if(instance==null){instance=new SingletonDemo();}}}return instance;}
}
//kotlin實現(xiàn)
class SingletonDemo private constructor() {companion object {val instance: SingletonDemo by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {SingletonDemo() }}
}
  • 靜態(tài)內(nèi)部類式
//Java實現(xiàn)
public class SingletonDemo {private static class SingletonHolder{private static SingletonDemo instance=new SingletonDemo();}private SingletonDemo(){System.out.println("Singleton has loaded");}public static SingletonDemo getInstance(){return SingletonHolder.instance;}
}
//kotlin實現(xiàn)
class SingletonDemo private constructor() {companion object {val instance = SingletonHolder.holder}private object SingletonHolder {val holder= SingletonDemo()}}

5. 了解Data Class嗎?

數(shù)據(jù)類,相當于MWM模式下的model類,相當于java自動重寫了equals/hashCode方法、get()方法、set()方法(如果是可寫入的)、
toString方法、componentN方法、copy()方法,注意get/set方法是kotlin中的類都會為屬性自動生成的方法,和數(shù)據(jù)類沒關(guān)系。

  • equals/hashCode:equals方法重寫使對象的內(nèi)容一致則返回true,hashCode方法重寫使對象的內(nèi)容一致則nashCode值也一致。

注意: 在Kotlin中有== 和 ===,==比較的對象內(nèi)容,===比較的是對象的引用地址

  • toString:重寫此方法為類和屬性值的內(nèi)容,如:“User(name=John,age=42)”
  • componentN:編譯器為數(shù)據(jù)類(data class)自動聲明componentN(O函數(shù),可直接用解構(gòu)Q聲明,如下:
var girl1:Girl=Girl("嫚嫚"29,160"廊坊")
var (a,b,c,d)=girl1
println("$a,$b,c,$d")
在Kotlin中所謂的解構(gòu)就是將一個類對象中的參數(shù)拆開來,成為一個一個單獨的變量,從而來使用這些單獨的變量進行操作。

copy:復制對象使用,當要復制一個對象,只改變一些屬性,但其余不變,copy就是為此而生

6. 了解作用域函數(shù)嗎?

  • wth:不是T的擴展函數(shù),需要傳入對象進去,不能判空,最后一行是返回值。
  • run:是T的擴展函數(shù),內(nèi)部使用this,最后一行是返回值。
  • apply:是T的擴展函數(shù),內(nèi)部使用this,返回值是調(diào)用本身。
  • let:是T的擴展函數(shù),內(nèi)部使用it,當然可以自定義名稱(通過修改ambda表達式參數(shù)),最后一行是返回值。
  • also:是T的擴展函數(shù),和let一樣內(nèi)部使用it,返回值是調(diào)用本身。

使用場景:

  • 用于初始化對象或更改對象屬性,可使用apply
  • 如果將數(shù)據(jù)指派給接收對象的屬性之前驗證對象,可使用also
  • 如果將對象進行空檢查并訪問或修改其屬性,可使用let
  • 如果想要計算某個值,或者限制多個本地變量的范圍,則使用run

擴展函數(shù)原理:
擴展函數(shù)實際上就是一個對應Jva中的靜態(tài)函數(shù),這個靜態(tài)函數(shù)參數(shù)為接收者類型的對象,然后利用這個對象就可以訪問這個類
中的成員屬性和方法了,并且最后返回一個這個接收者類型對象本身。這樣在外部感覺和使用類的成員函數(shù)是一樣的。

7. 你覺得Kotlin與Java混合開發(fā)時需要注意哪些問題?

  • kotlin調(diào)用java的時候,如果java返回值可能為null那就必須加上@nullable否則kotlin無法識別,也就不會強制你做非空處理,一旦java返回了null那么必定會出現(xiàn)null指針異常,加上@nullable注解之后kotlin就能識別到java方法可能會返回null,編譯器就能會知道,并且強制你做非null處理,這也就是kotlin的空安全。

8. 知道什么是inline ,noinline和crossinline函數(shù)嗎?

  • 內(nèi)聯(lián)函數(shù) inLine 作用是可以在編譯kotlin文件時直接把內(nèi)聯(lián)函數(shù)執(zhí)行過程放在調(diào)用此內(nèi)聯(lián)函數(shù)的位置,避免了java中多調(diào)用
    方法的操作,減少性能消耗。一方面可以減少方法調(diào)用棧幀的層級,一方面可以避免lambda表達式和高階函數(shù)運行時的效率損失:每個函數(shù)都是一個對象,并且會捕獲一個閉包。即那些在函數(shù)體內(nèi)會訪問到的變量。
    內(nèi)存分配和虛擬調(diào)用(對于函數(shù)和類)會引入運行時間開銷,但是通過內(nèi)聯(lián)化表達式可以消除這類的開銷
class TestMain(var a: Int, var b: String) {data class User(var age: Int, var sex: String);fun main() {calculate {System.out.println("調(diào)用方法體")}}fun calculate(method: () -> Unit) {System.out.println("calculate 前")method()System.out.println("calculate 后")}查看字節(jié)碼LINENUMBER 43 L1GETSTATIC java/lang/System.out : Ljava/io/PrintStream;LDC "calculate \u524d"INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)VL2LINENUMBER 44 L2ALOAD 1INVOKEINTERFACE kotlin/jvm/functions/Function0.invoke ()Ljava/lang/Object; (itf) //多了一次方法調(diào)用POPL3LINENUMBER 45 L3GETSTATIC java/lang/System.out : Ljava/io/PrintStream;LDC "calculate \u540e"INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)VL4LINENUMBER 46 L4RETURNL5
改為inline后inline fun calculate(method: () -> Unit) {System.out.println("calculate 前")method()System.out.println("calculate 后")}public final main()VL0LINENUMBER 15 L0ALOAD 0ASTORE 1L1ICONST_0ISTORE 2L2LINENUMBER 56 L2GETSTATIC java/lang/System.out : Ljava/io/PrintStream;LDC "calculate \u524d"INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)VL3LINENUMBER 57 L3L4ICONST_0ISTORE 3L5LINENUMBER 16 L5    //直接打印沒有方法調(diào)用GETSTATIC java/lang/System.out : Ljava/io/PrintStream;LDC "\u8c03\u7528\u65b9\u6cd5\u4f53"INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)VL6LINENUMBER 17 L6NOPL7L8LINENUMBER 58 L8GETSTATIC java/lang/System.out : Ljava/io/PrintStream;LDC "calculate \u540e"INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)VL9LINENUMBER 59 L9NOPL10LINENUMBER 18 L10RETURNL11LOCALVARIABLE $i$a$-calculate-TestMain$main$1 I L5 L7 3LOCALVARIABLE this_$iv Lcom/example/test/TestMain; L1 L10 1LOCALVARIABLE $i$f$calculate I L2 L10 2LOCALVARIABLE this Lcom/example/test/TestMain; L0 L11 0MAXSTACK = 2MAXLOCALS = 4
  • noinline noinline 字面意思是“不內(nèi)聯(lián)”,用于標記 inline 函數(shù)中的函數(shù)類型參數(shù)。被標記的函數(shù)類型參數(shù)不會被內(nèi)聯(lián),即不會如上示例所述,進行代碼鋪平,其依舊是個對象。那么關(guān)閉內(nèi)聯(lián)優(yōu)化,有什么用呢?我們可以看下面一個例子:
    在這里插入圖片描述
    我們有時會碰到返回值就是函數(shù)類型參數(shù)這種狀況,但是 inline 函數(shù)已經(jīng)將 hello() 中的 block 代碼鋪平,即本來 block 是個對象,現(xiàn)在被 inline 優(yōu)化,直接消除了,那還怎么返回呢?所以上述示例是錯誤的,同時我們也能在 IDE 中發(fā)現(xiàn) return 直接報錯了。這時就輪到 noinline 出場,關(guān)閉針對函數(shù)類型參數(shù)的內(nèi)聯(lián)優(yōu)化,使 block 依然作為一個對象被使用,如此就可以正常 return 了。正確示例如下:
    在這里插入圖片描述
  • crossinline crossinline 字面意思是交叉內(nèi)聯(lián),也是作用于內(nèi)聯(lián)函數(shù)的函數(shù)類型參數(shù)上,其用途就是強化函數(shù)類型參數(shù)的內(nèi)聯(lián)優(yōu)化,使之能被間接調(diào)用,并且被 crossinline 標記的 Lambda 中不能使用 return。
    有這么一個需求,我們需要在UI線程中去執(zhí)行內(nèi)聯(lián)函數(shù)中的某個代碼塊,這個需求應該不過分吧?常規(guī)寫法一般如下:
// 錯誤示例
inline fun hello(block: () -> Unit) {println("Say Hello!")runOnUiThread {block()}
}fun main() {hello {println("Bye!")return}println("Continue")
}

這其實就是在內(nèi)聯(lián)函數(shù)中間接調(diào)用函數(shù)類型參數(shù),說“間接”是因為本來 block 的控制權(quán)是在 hello() 中,其外層是 hello(),這么一寫,控制權(quán)就被 runOnUiThread 奪走了,外層變成了 runOnUiThread。而如果此時,我們在 hello() 的 Lambda 表達式中加個 return,那么就會出現(xiàn)一個問題, return 無法結(jié)束 main()。因為遵循 inline 規(guī)則,最后編譯出的代碼大致是這樣的:
在這里插入圖片描述
非常明顯,return 結(jié)束的是 runOnUiThread 中的 Runnable 對象,而不是 main()。那這樣與之前的規(guī)則不就沖突了嗎?所以事實上,這種間接調(diào)用寫法是不被允許的,IDE 會給你一個刺眼的紅線。那如果我們一定要間接調(diào)用該怎么做呢?這時就輪到 crossinline 出場了,正確示例如下:

// 正確示例
inline fun hello(crossinline block: () -> Unit) {println("Say Hello!")runOnUiThread {block()}
}fun main() {hello {println("Bye!")}
}

我們直接給 block 加上關(guān)鍵字 crossinline,這樣就允許間接調(diào)用了。

9. Kotlin中的構(gòu)造方法

  • 1.概要簡述

    • 1.kotlin中構(gòu)造函數(shù)分為主構(gòu)造和次級構(gòu)造兩類
    • 2.使用關(guān)鍵飼constructori標記次級構(gòu)造函數(shù),部分情況可省略
    • 3.init關(guān)鍵詞用于初始化代碼塊,注意與構(gòu)造函數(shù)的執(zhí)行順序,類成員的初始化順序
    • 4.繼承,擴展時候的構(gòu)造函數(shù)調(diào)用邏輯
    • 5.特殊的類如data class、.object/,componain object、.sealed classs等構(gòu)造函數(shù)情況與繼承問題
    • 6.構(gòu)造函數(shù)中的形參聲明情況
  • 2.詳細說明

    • 主次構(gòu)造函數(shù)
    • 1.kotlin中任何class(包括ob ject/data class/sealed class)都有一個默認的無參構(gòu)造函數(shù)
    • 2.如果顯式的聲明了構(gòu)造函數(shù),默認的無參構(gòu)造函數(shù)就失效了。
    • 3.主構(gòu)造函數(shù)寫在classi聲明處,可以有訪問權(quán)限修飾符private,publics等,且可以省略constructor關(guān)鍵字。
    • 4.若顯式的在class內(nèi)聲明了次級構(gòu)造函數(shù),就需要委托調(diào)用主構(gòu)造函數(shù)。
    • 5.若在class內(nèi)顯式的聲明處所有構(gòu)造函數(shù)(也就是沒有了所謂的默認主構(gòu)造),這時候可以不用依次調(diào)用主構(gòu)造函
      數(shù)。例如繼承Viw實現(xiàn)自定義控件時,三四個構(gòu)造函數(shù)同時顯示聲明。
  • init初始化代碼塊

    • kotlin中若存在主構(gòu)造函數(shù),其不能有代碼塊執(zhí)行,init起到類似作用,在類初始化時侯執(zhí)行相關(guān)的代碼塊。
    • 1.init代碼塊優(yōu)先于次級構(gòu)造函數(shù)中的代碼塊執(zhí)行。
    • 2.即使在類的繼承體系中,各自的init也是優(yōu)先于構(gòu)造函數(shù)執(zhí)行。
    • 3.在主構(gòu)造函數(shù)中,形參加有var/val,那么就變成了成員屬性的聲明。這些屬性聲明是早于init代碼塊的。
  • 特殊類

    • 1.object/companion object是對象示例,作為單例類或者伴生對象,沒有構(gòu)造函數(shù)。
    • 2.data class要求必須有一個含有至少一個成員屬性的主構(gòu)造函數(shù),其余方面和普通類相同。
    • 3.sealed class,只是聲明類似以抽象類一般,可以有主構(gòu)造函數(shù),含參無參以及次級構(gòu)造等。
      在這里插入圖片描述

10. 說說Kotlin中的Any與Java中的Object有何異同?

  • 同:都是頂級父類
  • 異:成員方法不同
    Any只聲明了toString()、hashCode(O和equals()作為成員方法。

我們思考下,為什么Kotlin設計了一個Any?
當我們需要和Java互操作的時候,Kotlin把Java方法參數(shù)和返回類型中用到的Object類型看作Any,這個Any的設計是Kotlin兼容
Java時的一種權(quán)衡設計。
所有Java引用類型在Kotlin中都表現(xiàn)為平臺類型。當在Kotlin中處理平臺類型的值的時候,它既可以被當做可空類型來處理,也可以被當做非空類型來操作。
試想下,如果所有來自Jva的值都被看成非空,那么就容易寫出比較危險的代碼。反之,如果Java值都強制當做可空,則會導致大量的null檢查。綜合考量,平臺類型是一種折中的設計方案。

11. 協(xié)程Flow是什么,有哪些應用場景?

  • 協(xié)程Flow:Kotlin協(xié)程中使用掛起函數(shù)可以實現(xiàn)非阻塞地執(zhí)行任務并將結(jié)果返回回來,但是只能返回單個計算結(jié)果。但是如果希望有多個計算結(jié)果返回回來,則可以使用Flow。
    應用場景:多個數(shù)據(jù)流執(zhí)行的情況下。

12. 協(xié)程Flow的冷流和熱流是什么?

  • 熱數(shù)據(jù)很迫切,它們盡可能快的生產(chǎn)元素并存儲它們。它們創(chuàng)造的元素獨立于它們的消費者,它們是集合(List、Set)和channel
  • 冷數(shù)據(jù)流是惰性的,它們在終端操作上按需處理元素,所有中間函數(shù)知識定義應該做什么(通常是用裝飾模式),它們通常不存儲元素,而是根據(jù)需要創(chuàng)建元素,它們的運算次數(shù)很少,可以是無限的,它們創(chuàng)建、處理元素的過程通常和消費過程緊挨著。這些元素是Sequence、Java Stream,Flow和RxJava流(Observable、Single等)
  • 協(xié)程FIow中的熱流是channelFlow,冷流是FIow
fun main() = runBlocking {val time = measureTimeMillis {
//        equeneFlow() //同步 1秒左右asyncFlow() //異步700多毫秒}print("cost $time")
}
//異步的
private suspend fun asyncFlow() {channelFlow {for (i in 1..5) {delay(100)send(i)}}.collect {delay(100)println(it)}
}
//同步的
private suspend fun equeneFlow() {flow<Int> {for (i in 1..5) {delay(100)emit(i)}}.collect {delay(100)println(it)}
}

13. 談談Kotlin中的Sequence,為什么它處理集合操作更加高效?

集合操作低效在哪?
處理集合時性能損耗的最大原因是循環(huán)。集合元素迭代的次數(shù)越少性能越好。

list.map { it ++ }.filter { it % 2 == 0 }.count { it < 3 } 

反編譯一下,你會發(fā)現(xiàn):Kotlin編譯器會創(chuàng)建三個while循環(huán)。
Sequences減少了循環(huán)次數(shù)
Sequences提高性能的秘密在于這三個操作可以共享同一個迭代器(iterator),只需要一次循環(huán)即可完成。Sequences允許map轉(zhuǎn)換一個元素后,立馬將這個元素傳遞給filter操作,而不是像集合(lists)那樣,等待所有的元素都循環(huán)完成了map操作后,用一個新的集合存儲起來,然后又遍歷循環(huán)從新的集合取出元素完成filter操作。
Sequences是懶惰的
上面的代碼示例,map、filter.、count都是屬于中間操作,只有等待到一個終端操作,如打印、sum()、average()、first()時才會開始工作

val list = listOf(1, 2, 3, 4, 5, 6)
val result = list.asSequence().map{ println("--map"); it * 2 }.filter { println("--filter");it % 3  == 0 }
println("go~")
println(result.average())

android

1. Activity啟動模式

  • standard 標準模式,每次都是新建Activity實例。
  • singleTop 棧頂復用。如果要啟動的Activity已經(jīng)處于任務棧頂,則直接復用不會新建Activity實例,此時會調(diào)用onNewIntent方法。如果棧內(nèi)不存在或者不在棧頂。則會新建Activity實例。
  • singleTask 棧內(nèi)單例。如果任務棧內(nèi)已經(jīng)存在Activity實例,則直接復用。如果不在棧頂,則把該activity實例之上的全部出棧,讓自身位于棧頂。此時會調(diào)用onNewIntent方法。
  • singleInstance 新建任務棧棧內(nèi)唯一。應用場景:來電話界面,即使來多個電話也只創(chuàng)建一個Activity;

2. Activity生命周期

在這里插入圖片描述

  • 啟動狀態(tài)(Starting):Activity的啟動狀態(tài)很短暫,當Activity啟動后便會進入運行狀態(tài)(Running)。
  • 運行狀態(tài)(Running):Activity在此狀態(tài)時處于屏幕最前端,它是可見、有焦點的,可以與用戶進行交互。如單擊、長按等事件。即使出現(xiàn)內(nèi)存不足的情況,Android也會先銷毀棧底的Activity,來確保當前的Activity正常運行。
  • 暫停狀態(tài)(Paused):在某些情況下,Activity對用戶來說仍然可見,但它無法獲取焦點,用戶對它操作沒有沒有響應,此時它處于暫停狀態(tài)。例如,當前Activity彈出Dialog,或者新啟動Activity為透明的Activity等情況。
  • 停止狀態(tài)(Stopped):當Activity完全不可見時,它處于停止狀態(tài),但仍然保留著當前的狀態(tài)和成員信息。如系統(tǒng)內(nèi)存不足,那么這種狀態(tài)下的Activity很容易被銷毀。
  • 銷毀狀態(tài)(Destroyed):當Activity處于銷毀狀態(tài)時,將被清理出內(nèi)存。

Activity的生命周期
在這里插入圖片描述

  • onCreate() : 在Activity創(chuàng)建時調(diào)用,通常做一些初始化設置,不可以執(zhí)行耗時操作。;
  • onNewIntent()*:注意 !!只有當 當前activity實例已經(jīng)處于任務棧頂,并且使用啟動模式為singleTop或者SingleTask再次啟動Activity時才會調(diào)用此方法。此時不會走OnCreate(),而是會執(zhí)行onNewIntent()。因為activity不需要創(chuàng)建而是直接復用。
  • onStart(): 在Activity即將可見時調(diào)用;可以做一些動畫初始化的操作。
  • onRestoreInstanceState()*:注意 !!當app異常退出重建時才會調(diào)用此方法??梢栽谠摲椒ㄖ谢謴鸵员4娴臄?shù)據(jù)。
  • onResume(): 在Activity已可見,獲取焦點開始與用戶交互時調(diào)用;當Activity第一次啟動完成或者當前Activity被遮擋住一部分(進入了onPause())重新回到前臺時調(diào)用,比如彈窗消失。當onResume()方法執(zhí)行完畢之后Activity就進入了運行狀態(tài)。根據(jù)官方的建議,此時可以做開啟動畫和獨占設備的操作。
  • onPause(): 在當前Activity被其他Activity覆蓋或鎖屏時調(diào)用;Activity停止但是當前Activity還是處于用戶可見狀態(tài),比如出現(xiàn)彈窗;在onPause()方法中不能進行耗時操作(當前Activity通過Intent啟動另一個Activity時,會先執(zhí)行當前Activity的onPause()方法,再去執(zhí)行另一個Activity的生命周期)
  • onSaveInstanceState():注意 !! 只有當app可能會異常銷毀時才會調(diào)用此方法保存activity數(shù)據(jù)。以便于activity重建時恢復數(shù)據(jù)
    Activity的onSaveInstanceState回調(diào)時機,取決于app的targetSdkVersion:
    targetSdkVersion低于11的app,onSaveInstanceState方法會在Activity.onPause之前回調(diào);
    targetSdkVersion低于28的app,則會在onStop之前回調(diào);
    28之后,onSaveInstanceState在onStop回調(diào)之后才回調(diào)。
  • onStop() : 在Activity完全被遮擋對用戶不可見時調(diào)用(在onStop()中做一些回收資源的操作)
  • onDestroy() :在Activity銷毀時調(diào)用;
  • onRestart() : 在Activity從停止狀態(tài)再次啟動時調(diào)用;處于stop()狀態(tài)也就是完全不可見的Activity重新回到前臺時調(diào)用(重新回到前臺不會調(diào)用onCreate()方法,因為此時Activity還未銷毀)

Activity橫豎屏切換生命周期
橫豎屏切換涉及到的是Activity的android:configChanges屬性;
android:configChanges可以設置的屬性值有:

orientation:消除橫豎屏的影響
keyboardHidden:消除鍵盤的影響
screenSize:消除屏幕大小的影響

  • 設置Activity的android:configChanges屬性為orientation或者orientation|keyboardHidden或者不設置這個屬性的時候,橫豎屏切換會重新調(diào)用各個生命周期方法,切橫屏時會執(zhí)行1次,切豎屏時會執(zhí)行1次;
  • 設置Activity的屬性為 android:configChanges=“orientation|keyboardHidden|screenSize” 時,橫豎屏切換不會重新調(diào)用各個生命周期方法,只會執(zhí)行onConfigurationChanged方法;

3. 了解Service嗎

Service一般用于沒有ui界面的長期服務。
Service有兩種啟動方式

  • StartService 這種方式啟動的Service生命周期和啟動實例無關(guān)。啟動后會一直存在,直到app退出,或者調(diào)用stopService或者stopSelf。生命周期為onCreate-》onStartCommand-》onDestroyed。
    • 多次啟動StartService。onStartCommand會調(diào)用多次
  • bindService 這種方式啟動的Service和生命周期會和調(diào)用者綁定。一旦調(diào)用者結(jié)束,Service也會一起結(jié)束。生命周期為onCreate-》onBind-》onUnbind-》onDestroyed

如何保證Service不被殺死

  • onStartCommand方式中,返回START_STICKY或者START_REDELIVER_INTENT
    • START_STICKY:如果返回START_STICKY,Service運行的進程被Android系統(tǒng)殺掉之后,Android系統(tǒng)會將該Service依然設置為started狀態(tài)(即運行狀態(tài)),會重新創(chuàng)建該Service。但是不再保存onStartCommand方法傳入的intent對象
    • START_NOT_STICKY:如果返回START_NOT_STICKY,表示當Service運行的進程被Android系統(tǒng)強制殺掉之后,不會重新創(chuàng)建該Service
    • START_REDELIVER_INTENT:如果返回START_REDELIVER_INTENT,其返回情況與START_STICKY類似,但不同的是系統(tǒng)會保留最后一次傳入onStartCommand方法中的Intent再次保留下來并再次傳入到重新創(chuàng)建后的Service onStartCommand方法中
  • 提高Service的優(yōu)先級: 在AndroidManifest.xml文件中對于intent-filter可以通過android:priority = "1000"這個屬性設置最高優(yōu)先級,1000是最高值,如果數(shù)字越小則優(yōu)先級越低,同時適用于廣播;
  • 在onDestroy方法里重啟Service: 當service走到onDestroy()時,發(fā)送一個自定義廣播,當收到廣播 時,重新啟動service;
  • 提升Service進程的優(yōu)先級。 進程優(yōu)先級由高到低:前臺進程 一》 可視進程 一》 服務進程 一》 后臺進程 一》 空進程
    可以使用 startForegroundservice放到前臺狀態(tài),這樣低內(nèi)存時,被殺死的概率會低一些; 系統(tǒng)廣播監(jiān)聽Service狀態(tài)將APK安裝到/system/app,變身為系統(tǒng)級應用。

4. 使用過broadcastReceiver嗎?

可分為標準廣播(無序廣播)有序廣播
按照作用范圍可分為全局廣播本地廣播
按照注冊方式可分為靜態(tài)廣播動態(tài)廣播

  • 標準廣播
    標準廣播(normal broadcasts)是一種完全異步執(zhí)行的廣播,在廣播發(fā)出之后,所有的BroadcastReceiver幾乎都會在同一時刻接收到收到這條廣播消息,因此它們之間沒有任何先后順序可言。這種廣播的效率會比較高,但同時也意味著它是無法被截斷的。
  • 有序廣播
    有序廣播(ordered broadcasts)是一種同步執(zhí)行的廣播,在廣播發(fā)出之后,同一時刻只會有一個BroadcastReceiver能夠收到這條廣播消息,當這個BroadcastReceiver中的邏輯執(zhí)行完畢后,廣播才會繼續(xù)傳遞。所以此時的BroadcastReceiver是有先后順序的,優(yōu)先級高的BroadcastReceiver就可以先收到廣播消息,并且前面的BroadcastReceiver還可以截斷正在傳遞的廣播,這樣后面的BroadcastReceiver就無法收到廣播消息了。
  • 本地廣播:發(fā)送的廣播事件不被其他應用程序獲取,也不能響應其他應用程序發(fā)送的廣播事件。本地廣播只能被動態(tài)注冊,不能靜態(tài)注冊。動態(tài)注冊或發(fā)送時時需要用到LocalBroadcastManager。
  • 全局廣播:發(fā)送的廣播事件可被其他應用程序獲取,也能響應其他應用程序發(fā)送的廣播事件(可以通過 exported–是否監(jiān)聽其他應用程序發(fā)送的廣播 在清單文件中控制) 全局廣播既可以動態(tài)注冊,也可以靜態(tài)注冊。
  • 靜態(tài)廣播
    靜態(tài)廣播在清單文件AndroidMainfest.xml中注冊,生命周期隨系統(tǒng),不受Activity生命周期影響,即使進程被殺死,仍然能收到廣播,因此也可以通過注冊靜態(tài)廣播做一些拉起進程的事。隨著Android版本的增大,Android系統(tǒng)對靜態(tài)廣播的限制也越來越嚴格,一般能用動態(tài)廣播解決的問題就不要用靜態(tài)廣播。
  • 動態(tài)廣播
    動態(tài)廣播不需要在AndroidManifest.xml文件中進行注冊,動態(tài)注冊的廣播受Activity聲明周期的影響,Activity消亡,廣播也就不復存在。動態(tài)廣播在需要接受廣播的Activity中進行注冊和解注冊。

5. 說說你對handler的理解

Handler是Android用來解決線程間通訊問題的消息機制。Handler消息機制分為四個部分。

  • Handler 消息的發(fā)送者處理者
  • Message 消息實體 消息的載體和攜帶者。
  • MessageQueen 消息隊列,使用雙向鏈表實現(xiàn),是存放消息的隊列。
  • Looper 消息循環(huán)器,不停的從消息隊列中中取出消息。

如何使用Handler?

  • 使用Handler需要一個Looper環(huán)境,在主線程直接新建Handler實例然后實現(xiàn)handleMessage方法,然后在需要發(fā)送消息的地方,使用handler.sendMessage等方法即可。
private static class MyHandler extends Handler {private final WeakReference<MainActivity> mTarget;public MyHandler(MainActivity activity) {mTarget = new WeakReference<MainActivity>(activity);}@Overridepublic void handleMessage(@NonNull Message msg) {super.handleMessage(msg);HandlerActivity activity = weakReference.get();super.handleMessage(msg);if (null != activity) {//執(zhí)行業(yè)務邏輯if (msg.what == 0) {Log.e("myhandler", "change textview");MainActivity ma = mTarget.get();ma.textView.setText("hahah");}Toast.makeText(activity,"handleMessage",Toast.LENGTH_SHORT).show();}}}private Handler handler1 = new MyHandler(this);new Thread(new Runnable() {@Overridepublic void run() {handler1.sendEmptyMessage(0);}}).start();
  • 在子線程使用需要先創(chuàng)建Looper環(huán)境,調(diào)用Looper.prepare(),然后再創(chuàng)建Handler。最后在調(diào)用Looper.loop()啟動消息循環(huán)。子線程Handler不使用時要調(diào)用handler.getLooper().quitSafely()退出Looper否則會阻塞。
	private static class MyHandler extends Handler {@Overridepublic void handleMessage(@NonNull Message msg) {super.handleMessage(msg);if (msg.what == 0) {Log.e("child thread", "receive msg from main thread");}}}private Handler handler1;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {     new Thread(new Runnable() {@Overridepublic void run() {Looper.prepare(); //準備Looper環(huán)境handler1 = new MyHandler();Looper.loop(); //啟動LooperLog.e("child thread", "child thread end");}}).start();handler1.sendEmptyMessage(0);handler1.getLooper().quitSafely();//子線程Handler不用時,退出Looper
}

主線程使用Handler為什么不用Looper.prepare()?

因為在app啟動時,ActivityThread的Main方法里幫我們調(diào)用了Looper.prepareMainLooper()。并且最后調(diào)用了Looper.loop()
啟動了主線程。

public static void main(String[] args) {Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");// Install selective syscall interceptionAndroidOs.install();// CloseGuard defaults to true and can be quite spammy.  We// disable it here, but selectively enable it later (via// StrictMode) on debug builds, but using DropBox, not logs.CloseGuard.setEnabled(false);Environment.initForCurrentUser();// Make sure TrustedCertificateStore looks in the right place for CA certificatesfinal File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());TrustedCertificateStore.setDefaultUserDirectory(configDir);// Call per-process mainline module initialization.initializeMainlineModules();Process.setArgV0("<pre-initialized>");Looper.prepareMainLooper();// Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line.// It will be in the format "seq=114"long startSeq = 0;if (args != null) {for (int i = args.length - 1; i >= 0; --i) {if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {startSeq = Long.parseLong(args[i].substring(PROC_START_SEQ_IDENT.length()));}}}ActivityThread thread = new ActivityThread();thread.attach(false, startSeq);if (sMainThreadHandler == null) {sMainThreadHandler = thread.getHandler();}if (false) {Looper.myLooper().setMessageLogging(newLogPrinter(Log.DEBUG, "ActivityThread"));}// End of event ActivityThreadMain.Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);Looper.loop();throw new RuntimeException("Main thread loop unexpectedly exited");}

簡述一下Handler的工作流程

  • Handler使用SendMessage或者post等方法。最終都會調(diào)用MessageQueue的enqueueMessage()方法。將消息按照執(zhí)行時間先后順序入隊。
  • Looper里面是個死循環(huán),不停地在隊列中通過MessageQueue.next()方法取出消息,取出消息后,通過msg.target.dispatchMessage() 方法分發(fā)消息。先交給msg消息的runnable處理,再交給Handler的Callable處理,最后再交給Handler實現(xiàn)的handleMessage方法處理
    public void dispatchMessage(@NonNull Message msg) {if (msg.callback != null) {handleCallback(msg);} else {if (mCallback != null) {if (mCallback.handleMessage(msg)) {return;}}handleMessage(msg);}}

一個線程中最多有多少個Handler,Looper,MessageQueue?

  • 一個線程可以有多個Handler
  • 一個handler只能有一個Looper和一個MessageQueen
    因為創(chuàng)建Handler必須有Looper環(huán)境,而Looper只能通過Looper.prepare和Looper.prepareMainLooper來創(chuàng)建。同時將Looper實例存放到線程局部變量sThreadLocal(ThreadLocal)中,也就是每個線程有自己的Looper。在創(chuàng)建Looper的時候也創(chuàng)建了該線程的消息隊列,prepareMainLooper會判斷sMainLooper是否有值,如果調(diào)用多次,就會拋出異常,所以主線程的Looper和MessageQueue只會有一個。同理子線程中調(diào)用Looper.prepare()時,會調(diào)用prepare(true)方法,如果多次調(diào)用,也會拋出每個線程只能由一個Looper的異常,總結(jié)起來就是每個線程中只有一個Looper和MessageQueue。
    public static void prepare() {prepare(true);}private static void prepare(boolean quitAllowed) {if (sThreadLocal.get() != null) {throw new RuntimeException("Only one Looper may be created per thread");}sThreadLocal.set(new Looper(quitAllowed));}

Looper死循環(huán)為什么不會導致應用ANR、卡死,會耗費大量資源嗎?

線程其實就是一段可執(zhí)行的代碼,當可執(zhí)行的代碼執(zhí)行完成后,線程的生命周期便該終止了,線程退出。而對于主線程,我們是絕不希望會被運行一段時間,自己就退出,那么如何保證能一直存活呢?簡單做法就是可執(zhí)行代碼是能一直執(zhí)行下去的,死循環(huán)便能保證不會被退出

  • ANR 產(chǎn)生的原因是主線程沒有及時響應用戶的操作。也就是主線程執(zhí)行某個耗時操作來不及處理UI消息。
  • 而Looper一直循環(huán),就是在不斷的檢索消息,與主線程無法響應用戶操作沒有任何沖突
  • Android是基于消息處理機制的,用戶的行為都在這個Looper循環(huán)中,正是有了主線程Looper的不斷循環(huán),才有app的穩(wěn)定運行。
  • 簡單來說looper的阻塞表明沒有事件輸入,而ANR是由于有事件沒響應導致,所以looper的死循環(huán)并不會導致應用卡死。

主線程的死循環(huán)并不消耗 CPU 資源,這里就涉及到 Linux pipe/epoll機制,簡單說就是在主線程的 MessageQueue 沒有消息時,便阻塞在 loop 的 queue.next() 中的 nativePollOnce() 方法里,此時主線程會釋放 CPU 資源進入休眠狀態(tài),直到下個消息到達或者有事務發(fā)生,通過往 pipe 管道寫端寫入數(shù)據(jù)來喚醒主線程工作。這里采用的 epoll 機制,是一種IO多路復用機制,可以同時監(jiān)控多個描述符,當某個描述符就緒(讀或?qū)懢途w),則立刻通知相應程序進行讀或?qū)懖僮?#xff0c;本質(zhì)同步I/O,即讀寫是阻塞的。 所以說,主線程大多數(shù)時候都是處于休眠狀態(tài),并不會消耗大量CPU資源。

Handler同步屏障了解嗎

同步屏障是為了保證異步消息的優(yōu)先執(zhí)行,一般是用于UI繪制消息,避免主線程消息太多,無法及時處理UI繪制消息,導致卡頓。

  • 同步消息 一般的handler發(fā)送的消息都是同步消息
  • 異步消息 Message標記為異步的消息
    • 可以調(diào)用 Message#setAsynchronous() 直接設置為異步 Message
    • 可以用異步 Handler 發(fā)送
  • 同步屏障 在 MessageQueue 的 某個位置放一個 target 屬性為 null 的 Message ,確保此后的非異步 Message 無法執(zhí)行,只能執(zhí)行異步 Message。

當 Looper輪循MessageQueue 遍歷 Message發(fā)現(xiàn)建立了同步屏障的時候,會去跳過其他Message,讀取下個 async 的 Message 并執(zhí)行,屏障移除之前同步 Message 都會被阻塞。
比如屏幕刷新 Choreographer 就使用到了同步屏障 ,確保屏幕刷新事件不會因為隊列負荷影響屏幕及時刷新。
注意: 同步屏障的添加或移除 API 并未對外公開,App 需要使用的話需要依賴反射機制

Handler 為什么可能導致內(nèi)存泄露?如何避免?

持有 Activity 實例的匿名內(nèi)部類或內(nèi)部類的 生命周期 應當和 Activity 保持一致,否則產(chǎn)生內(nèi)存泄露的風險。

如果 Handler 使用不當,將造成不一致,表現(xiàn)為:匿名內(nèi)部類或內(nèi)部類寫法的 Handler、Handler$Callback、Runnable,或者Activity 結(jié)束時仍有活躍的 Thread 線程或 Looper 子線程

具體在于:異步任務仍然活躍或通過發(fā)送的 Message 尚未處理完畢,將使得內(nèi)部類實例的 生命周期被錯誤地延長 。造成本該回收的 Activity 實例 被別的 Thread 或 Main Looper 占據(jù)而無法及時回收 (活躍的 Thread 或 靜態(tài)屬性 sMainLooper 是 GC Root 對象)
建議的做法:

  • 無論是 Handler、Handler$Callback 還是 Runnable,盡量采用 靜態(tài)內(nèi)部類 + 弱引用 的寫法,確保盡管發(fā)生不當引用的時候也可以因為弱引用能清楚持有關(guān)系
  • 另外在 Activity 銷毀的時候及時地 終止 Thread、停止子線程的 Looper 或清空 Message ,確保徹底切斷 Activity 經(jīng)由 Message 抵達 GC Root 的引用源頭(Message 清空后會其與 Handler 的引用關(guān)系,Thread 的終止將結(jié)束其 GC Root 的源頭)

Handler是如何實現(xiàn)線程間通訊的

  • handler是消息的發(fā)送者也是處理者。發(fā)送消息時,msg.target會標記為自身。插入MessageQueen后,被Looper取出后會通過msg.target.dispatchMessage去分發(fā)給對應的Handler去處理。

Handler消息處理的優(yōu)先級

    public void dispatchMessage(@NonNull Message msg) {if (msg.callback != null) {handleCallback(msg);} else {if (mCallback != null) {if (mCallback.handleMessage(msg)) {return;}}handleMessage(msg);}}

可以看出優(yōu)先級是Message.CallBack->Handler.callback->Handler.handleMessage
有時候面試官也會問Runnable->Callable->handleMessage
post方法就是runnable
Handler構(gòu)造傳入Callback就是Callable
send方法是handleMessage

如何正確或Message實例

  • 通過 Message 的靜態(tài)方法 Message.obtain() 獲取;
  • 通過 Handler 的公有方法 handler.obtainMessage()
  • 默認大小是50

Message使用享元設計模式,里面有一個spool指向一個Message對象,還有一個next指向下一個Message,維護了一個鏈表實現(xiàn)的對象池,obtain的時候在表頭頭取Message,在Message回收的時候在表頭添加一個Message。

Android 為什么不允許并發(fā)訪問 UI?

Android 中 UI 非線程安全,并發(fā)訪問的話會造成數(shù)據(jù)和顯示錯亂。
此限制的檢查始于ViewRootImpl#checkThread(),其會在刷新等多個訪問 UI 的時機被調(diào)用,去檢查當前線程,非主線程的話拋出異常。(實際上并不是檢查主線程。而是檢查UI的更新線程是否與UI的創(chuàng)建線程一致,因為UI是在主線程創(chuàng)建的,所以也只能在主線程更新)
而 ViewRootImpl 的創(chuàng)建在 onResume() 之后,也就是說如果在 onResume() 執(zhí)行前啟動線程訪問 UI 的話是不會報錯的。

了解ThreadLocal嗎

  • Thread中會維護一個類似HashMap的東西,然后用ThreadLocal對象作為key,value就是要存儲的變量值,這樣就保證了存儲數(shù)據(jù)的唯一性)
  • ThreadLocal為每個線程都提供了變量的副本,使得每個線程在某一時間訪問到的并非同一個對象,這樣就隔離了多個線程對數(shù)據(jù)的數(shù)據(jù)共享。
  • ThreadLocal 內(nèi)部通過 ThreadLocalMap 持有 Looper,key 為 ThreadLocal 實例本身,value 即為 Looper 實例
    每個 Thread 都有一個自己的 ThreadLocalMap,這樣可以保證每個線程對應一個獨立的 Looper 實例,進而保證 myLooper() 可以獲得線程獨有的 Looper。讓每個線程方便程獲取自己的 Looper 實例
    在這里插入圖片描述

ThreadLocal與內(nèi)存泄漏

  • 在線程池中使用ThreadLocal可能會導致內(nèi)存泄漏,原因是線程池中線程的存活時間太長,往往和程序都是同生共死的,這就意味著Thread持有的ThreadLocalMap一直都不會被回收,再加上ThreadLocalMap中的Entry對ThreadLocal是弱引用,所以只要ThreadLocal結(jié)束了自己的生命周期是可以被回收掉的。但是Entry中的Value卻是被Entry強引用的,所以即便Value的生命周期結(jié)束了,Value也是無法被回收的,從而導致內(nèi)存泄漏。
ExecutorService es;
ThreadLocal tl;
es.execute(()->{
//ThreadLocal增加變量
tl.set(obj);
try{
//業(yè)務代馮
}finally{
//于動消ThreadLocal
tl.remove();}
})

Message 的執(zhí)行時刻如何管理

  • 發(fā)送的 Message 都是按照執(zhí)行時刻 when 屬性的先后管理在 MessageQueue 里
    延時 Message 的 when 等于調(diào)用的當前時刻和 delay 之和
    非延時 Message 的 when 等于當前時刻(delay 為 0)
  • 插隊 Message 的 when 固定為 0,便于插入隊列的 head之后 MessageQueue 會根據(jù) 讀取的時刻和 when 進行比較將 when 已抵達的出隊,尚未抵達的計算出 當前時刻和目標 when 的插值 ,交由 Native 等待對應的時長,時間到了自動喚醒繼續(xù)進行 Message 的讀取
  • 事實上,無論上述哪種 Message 都不能保證在其對應的 when 時刻執(zhí)行,往往都會延遲一些!因為必須等當前執(zhí)行的 Message 處理完了才有機會讀取隊列的下一個 Message。
    比如發(fā)送了非延時 Message,when 即為發(fā)送的時刻,可它們不會立即執(zhí)行。都要等主線程現(xiàn)有的任務(Message)走完才能有機會出隊,而當這些任務執(zhí)行完 when 的時刻已經(jīng)過了。假使隊列的前面還有其他 Message 的話,延遲會更加明顯!

Looper等待如何準確喚醒的?

讀取合適 Message 的 MessageQueue#next() 會因為 Message 尚無或執(zhí)行條件尚未滿足進行兩種等的等待:

  • 無限等待
    尚無 Message(隊列中沒有 Message 或建立了同步屏障但尚無異步 Message)的時候,調(diào)用 Natvie 側(cè)的 pollOnce() 會傳入?yún)?shù) -1 。
    Linux 執(zhí)行 epoll_wait() 將進入無限等待,其等待合適的 Message 插入后調(diào)用 Native 側(cè)的 wake() 喚醒 fd 寫入事件觸發(fā)喚醒 MessageQueue 讀取的下一次循環(huán)

  • 有限等待
    有限等待的場合將下一個 Message 剩余時長作為參數(shù) 交給 epoll_wait(),epoll 將等待一段時間之后 自動返回 ,接著回到 MessageQueue 讀取的下一次循環(huán)。

Handler機制原理

  • Looper 準備和開啟輪循:
    尚無 Message 的話,調(diào)用 Native 側(cè)的 pollOnce() 進入 無限等待
    存在 Message,但執(zhí)行時間 when 尚未滿足的話,調(diào)用 pollOnce() 時傳入剩余時長參數(shù)進入 有限等待
    Looper#prepare() 初始化線程獨有的 Looper 以及 MessageQueue
    Looper#loop() 開啟 死循環(huán) 讀取 MessageQueue 中下一個滿足執(zhí)行時間的 Message

  • Message 發(fā)送、入隊和出隊:
    Native 側(cè)如果處于無限等待的話:任意線程向 Handler 發(fā)送 Message 或 Runnable 后,Message 將按照 when 條件的先后,被插入 Handler 持有的 Looper 實例所對應的 MessageQueue 中 適當?shù)奈恢?。MessageQueue 發(fā)現(xiàn)有合適的 Message 插入后將調(diào)用 Native 側(cè)的 wake() 喚醒無限等待的線程。這將促使 MessageQueue 的讀取繼續(xù) 進入下一次循環(huán) ,此刻 Queue 中已有滿足條件的 Message 則出隊返回給 Looper
    Native 側(cè)如果處于有限等待的話:在等待指定時長后 epoll_wait 將返回。線程繼續(xù)讀取 MessageQueue,此刻因為時長條件將滿足將其出隊

  • handler處理 Message 的實現(xiàn):
    Looper 得到 Message 后回調(diào) Message 的 callback 屬性即 Runnable,或依據(jù) target 屬性即 Handler,去執(zhí)行 Handler 的回調(diào)。存在 mCallback 屬性的話回調(diào) Handler$Callback反之,回調(diào) handleMessage()

6. 了解View繪制流程嗎?

Activity啟動走完onResume方法后,會進行window的添加。window添加過程會調(diào)用**ViewRootImpl的setView()方法,setView()方法會調(diào)用requestLayout()方法來請求繪制布局,requestLayout()方法內(nèi)部又會走到scheduleTraversals()方法,最后會走到performTraversals()**方法,接著到了我們熟知的測量、布局、繪制三大流程了。

  • 所有UI的變化都是走到ViewRootImpl的scheduleTraversals()方法。
    //ViewRootImpl.javavoid scheduleTraversals() {if (!mTraversalScheduled) {//此字段保證同時間多次更改只會刷新一次,例如TextView連續(xù)兩次setText(),也只會走一次繪制流程mTraversalScheduled = true;//添加同步屏障,屏蔽同步消息,保證VSync到來立即執(zhí)行繪制mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();//mTraversalRunnable是TraversalRunnable實例,最終走到run(),也即doTraversal();mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);if (!mUnbufferedInputDispatch) {scheduleConsumeBatchedInput();}notifyRendererOfFramePending();pokeDrawLockIfNeeded();}}final class TraversalRunnable implements Runnable {@Overridepublic void run() {doTraversal();}}final TraversalRunnable mTraversalRunnable = new TraversalRunnable();void doTraversal() {if (mTraversalScheduled) {mTraversalScheduled = false;//移除同步屏障mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);...//開始三大繪制流程performTraversals();...}}
  1. 首先使用mTraversalScheduled字段保證同時間多次更改只會刷新一次,例如TextView連續(xù)兩次setText(),也只會走一次繪制流程。
  2. 然后把當前線程的消息隊列Queue添加了同步屏障,這樣就屏蔽了正常的同步消息,保證VSync到來后立即執(zhí)行繪制,而不是要等前面的同步消息。后面會具體分析同步屏障和異步消息的代碼邏輯。
  3. 調(diào)用了mChoreographer.postCallback()方法,發(fā)送一個會在下一幀執(zhí)行的回調(diào),即在下一個VSync到來時會執(zhí)行TraversalRunnable–>doTraversal()—>performTraversals()–>繪制流程。

mChoreographer,是在ViewRootImpl的構(gòu)造方法內(nèi)使用Choreographer.getInstance()創(chuàng)建:

Choreographer mChoreographer;
//ViewRootImpl實例是在添加window時創(chuàng)建
public ViewRootImpl(Context context, Display display) {...mChoreographer = Choreographer.getInstance();...
}
    public static Choreographer getInstance() {return sThreadInstance.get();}private static final ThreadLocal<Choreographer> sThreadInstance =new ThreadLocal<Choreographer>() {@Overrideprotected Choreographer initialValue() {Looper looper = Looper.myLooper();if (looper == null) {//當前線程要有l(wèi)ooper,Choreographer實例需要傳入throw new IllegalStateException("The current thread must have a looper!");}Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);if (looper == Looper.getMainLooper()) {mMainInstance = choreographer;}return choreographer;}};
    private Choreographer(Looper looper, int vsyncSource) {mLooper = looper;//使用當前線程looper創(chuàng)建 mHandlermHandler = new FrameHandler(looper);//USE_VSYNC 4.1以上默認是true,表示 具備接受VSync的能力,這個接受能力就是FrameDisplayEventReceivermDisplayEventReceiver = USE_VSYNC? new FrameDisplayEventReceiver(looper, vsyncSource): null;mLastFrameTimeNanos = Long.MIN_VALUE;// 計算一幀的時間,Android手機屏幕是60Hz的刷新頻率,就是16msmFrameIntervalNanos = (long)(1000000000 / getRefreshRate());// 創(chuàng)建一個鏈表類型CallbackQueue的數(shù)組,大小為5,//也就是數(shù)組中有五個鏈表,每個鏈表存相同類型的任務:輸入、動畫、遍歷繪制等任務(CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL)mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];for (int i = 0; i <= CALLBACK_LAST; i++) {mCallbackQueues[i] = new CallbackQueue();}// b/68769804: For low FPS experiments.setFPSDivisor(SystemProperties.getInt(ThreadedRenderer.DEBUG_FPS_DIVISOR, 1));}

安排任務—postCallback
回頭看mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null)方法,注意到第一個參數(shù)是CALLBACK_TRAVERSAL,表示回調(diào)任務的類型,共有以下5種類型:

//輸入事件,首先執(zhí)行
public static final int CALLBACK_INPUT = 0;
//動畫,第二執(zhí)行
public static final int CALLBACK_ANIMATION = 1;
//插入更新的動畫,第三執(zhí)行
public static final int CALLBACK_INSETS_ANIMATION = 2;
//繪制,第四執(zhí)行
public static final int CALLBACK_TRAVERSAL = 3;
//提交,最后執(zhí)行,
public static final int CALLBACK_COMMIT = 4;

五種類型任務對應存入對應的CallbackQueue中,每當收到 VSYNC 信號時,Choreographer 將首先處理 INPUT 類型的任務,然后是 ANIMATION 類型,最后才是 TRAVERSAL 類型。
postCallback()內(nèi)部調(diào)用postCallbackDelayed(),接著又調(diào)用postCallbackDelayedInternal()

private void postCallbackDelayedInternal(int callbackType,Object action, Object token, long delayMillis) {...synchronized (mLock) {// 當前時間final long now = SystemClock.uptimeMillis();// 加上延遲時間final long dueTime = now + delayMillis;//取對應類型的CallbackQueue添加任務mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);if (dueTime <= now) {//立即執(zhí)行scheduleFrameLocked(now);} else {//延遲運行,最終也會走到scheduleFrameLocked()Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);msg.arg1 = callbackType;msg.setAsynchronous(true);mHandler.sendMessageAtTime(msg, dueTime);}}}

首先取對應類型的CallbackQueue添加任務,action就是mTraversalRunnable,token是null。CallbackQueue的addCallbackLocked()就是把 dueTime、action、token組裝成CallbackRecord后 存入CallbackQueue的下一個節(jié)點

然后注意到如果沒有延遲會執(zhí)行scheduleFrameLocked()方法,有延遲就會使用 mHandler發(fā)送MSG_DO_SCHEDULE_CALLBACK消息,并且注意到 使用msg.setAsynchronous(true)把消息設置成異步,這是因為前面設置了同步屏障,只有異步消息才會執(zhí)行。我們看下mHandler的對這個消息的處理:

private final class FrameHandler extends Handler {public FrameHandler(Looper looper) {super(looper);}@Overridepublic void handleMessage(Message msg) {switch (msg.what) {case MSG_DO_FRAME:// 執(zhí)行doFrame,即繪制過程doFrame(System.nanoTime(), 0);break;case MSG_DO_SCHEDULE_VSYNC://申請VSYNC信號,例如當前需要繪制任務時doScheduleVsync();break;case MSG_DO_SCHEDULE_CALLBACK://需要延遲的任務,最終還是執(zhí)行上述兩個事件doScheduleCallback(msg.arg1);break;}}
}
    void doScheduleCallback(int callbackType) {synchronized (mLock) {if (!mFrameScheduled) {final long now = SystemClock.uptimeMillis();if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {scheduleFrameLocked(now);}}}}
    private void scheduleFrameLocked(long now) {if (!mFrameScheduled) {mFrameScheduled = true;//開啟了VSYNCif (USE_VSYNC) {if (DEBUG_FRAMES) {Log.d(TAG, "Scheduling next frame on vsync.");}//當前執(zhí)行的線程,是否是mLooper所在線程if (isRunningOnLooperThreadLocked()) {//申請 VSYNC 信號scheduleVsyncLocked();} else {// 若不在,就用mHandler發(fā)送消息到原線程,最后還是調(diào)用scheduleVsyncLocked方法Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);msg.setAsynchronous(true);//異步mHandler.sendMessageAtFrontOfQueue(msg);}} else {// 如果未開啟VSYNC則直接doFrame方法(4.1后默認開啟)final long nextFrameTime = Math.max(mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);if (DEBUG_FRAMES) {Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");}Message msg = mHandler.obtainMessage(MSG_DO_FRAME);msg.setAsynchronous(true);//異步mHandler.sendMessageAtTime(msg, nextFrameTime);}}}
  1. 如果系統(tǒng)未開啟 VSYNC 機制,此時直接發(fā)送 MSG_DO_FRAME 消息到 FrameHandler。注意查看上面貼出的 FrameHandler 代碼,此時直接執(zhí)行 doFrame 方法。
  2. Android 4.1 之后系統(tǒng)默認開啟 VSYNC,在 Choreographer 的構(gòu)造方法會創(chuàng)建一個 FrameDisplayEventReceiver,scheduleVsyncLocked 方法將會通過它申請 VSYNC 信號。
  3. isRunningOnLooperThreadLocked 方法,其內(nèi)部根據(jù) Looper 判斷是否在原線程,否則發(fā)送消息到 FrameHandler。最終還是會調(diào)用 scheduleVsyncLocked 方法申請 VSYNC 信號。

FrameHandler的作用很明顯里了:發(fā)送異步消息(因為前面設置了同步屏障)。有延遲的任務發(fā)延遲消息、不在原線程的發(fā)到原線程、沒開啟VSYNC的直接走 doFrame 方法取執(zhí)行繪制。
申請和接受VSync
scheduleVsyncLocked 方法是如何申請 VSYNC 信號的。申請 VSYNC 信號后,信號到來時也是走doFrame() 方法:

    private void scheduleVsyncLocked() {mDisplayEventReceiver.scheduleVsync();}

調(diào)用mDisplayEventReceiver的scheduleVsync()方法,mDisplayEventReceiver是Choreographer構(gòu)造方法中創(chuàng)建,是FrameDisplayEventReceiver 的實例。 FrameDisplayEventReceiver是 DisplayEventReceiver 的子類,DisplayEventReceiver 是一個 abstract class:

    public DisplayEventReceiver(Looper looper, int vsyncSource) {if (looper == null) {throw new IllegalArgumentException("looper must not be null");}mMessageQueue = looper.getQueue();// 注冊VSYNC信號監(jiān)聽者mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,vsyncSource);mCloseGuard.open("dispose");}

在 DisplayEventReceiver 的構(gòu)造方法會通過 JNI 創(chuàng)建一個 IDisplayEventConnection 的 VSYNC 的監(jiān)聽者。
FrameDisplayEventReceiver的scheduleVsync()就是在 DisplayEventReceiver中:

    public void scheduleVsync() {if (mReceiverPtr == 0) {Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "+ "receiver has already been disposed.");} else {// 申請VSYNC中斷信號,會回調(diào)onVsync方法nativeScheduleVsync(mReceiverPtr);}}

scheduleVsync()就是使用native方法nativeScheduleVsync()去申請VSYNC信號。這個native方法就看不了了,只需要知道VSYNC信號的接受回調(diào)是onVsync()

    /*** 接收到VSync脈沖時 回調(diào)* @param timestampNanos VSync脈沖的時間戳* @param physicalDisplayId Stable display ID that uniquely describes a (display, port) pair.* @param frame 幀號碼,自增*/@UnsupportedAppUsagepublic void onVsync(long timestampNanos, long physicalDisplayId, int frame) {}

具體實現(xiàn)是在FrameDisplayEventReceiver中:

    private final class FrameDisplayEventReceiver extends DisplayEventReceiverimplements Runnable {private boolean mHavePendingVsync;private long mTimestampNanos;private int mFrame;public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {super(looper, vsyncSource);}@Overridepublic void onVsync(long timestampNanos, long physicalDisplayId, int frame) {// Post the vsync event to the Handler.// The idea is to prevent incoming vsync events from completely starving// the message queue.  If there are no messages in the queue with timestamps// earlier than the frame time, then the vsync event will be processed immediately.// Otherwise, messages that predate the vsync event will be handled first.long now = System.nanoTime();if (timestampNanos > now) {Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)+ " ms in the future!  Check that graphics HAL is generating vsync "+ "timestamps using the correct timebase.");timestampNanos = now;}if (mHavePendingVsync) {Log.w(TAG, "Already have a pending vsync event.  There should only be "+ "one at a time.");} else {mHavePendingVsync = true;}mTimestampNanos = timestampNanos;mFrame = frame;//將本身作為runnable傳入msg, 發(fā)消息后 會走run(),即doFrame(),也是異步消息Message msg = Message.obtain(mHandler, this);msg.setAsynchronous(true);mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);}@Overridepublic void run() {mHavePendingVsync = false;doFrame(mTimestampNanos, mFrame);}}

onVsync()中,將接收器本身作為runnable傳入異步消息msg,并使用mHandler發(fā)送msg,最終執(zhí)行的就是doFrame()方法了。

注意一點是,onVsync()方法中只是使用mHandler發(fā)送消息到MessageQueue中,不一定是立刻執(zhí)行,如何MessageQueue中前面有較為耗時的操作,那么就要等完成,才會執(zhí)行本次的doFrame()。
doFrame
VSync信號接收到后確實是走 doFrame()方法,那么就來看看Choreographer的doFrame()

    void doFrame(long frameTimeNanos, int frame) {final long startNanos;synchronized (mLock) {if (!mFrameScheduled) {return; // no work to do}...// 預期執(zhí)行時間long intendedFrameTimeNanos = frameTimeNanos;startNanos = System.nanoTime();// 超時時間是否超過一幀的時間(這是因為MessageQueue雖然添加了同步屏障,但是還是有正在執(zhí)行的同步任務,導致doFrame延遲執(zhí)行了)final long jitterNanos = startNanos - frameTimeNanos;if (jitterNanos >= mFrameIntervalNanos) {// 計算掉幀數(shù)final long skippedFrames = jitterNanos / mFrameIntervalNanos;if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {// 掉幀超過30幀打印Log提示Log.i(TAG, "Skipped " + skippedFrames + " frames!  "+ "The application may be doing too much work on its main thread.");}final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;...frameTimeNanos = startNanos - lastFrameOffset;}...           mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);// Frame標志位恢復mFrameScheduled = false;// 記錄最后一幀時間mLastFrameTimeNanos = frameTimeNanos;}try {// 按類型順序 執(zhí)行任務Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);mFrameInfo.markInputHandlingStart();doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);mFrameInfo.markAnimationsStart();doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);mFrameInfo.markPerformTraversalsStart();doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);} finally {AnimationUtils.unlockAnimationClock();Trace.traceEnd(Trace.TRACE_TAG_VIEW);}}
    void doCallbacks(int callbackType, long frameTimeNanos) {CallbackRecord callbacks;synchronized (mLock) {final long now = System.nanoTime();// 根據(jù)指定的類型CallbackkQueue中查找到達執(zhí)行時間的CallbackRecordcallbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(now / TimeUtils.NANOS_PER_MS);if (callbacks == null) {return;}mCallbacksRunning = true;//提交任務類型if (callbackType == Choreographer.CALLBACK_COMMIT) {final long jitterNanos = now - frameTimeNanos;if (jitterNanos >= 2 * mFrameIntervalNanos) {final long lastFrameOffset = jitterNanos % mFrameIntervalNanos+ mFrameIntervalNanos;if (DEBUG_JANK) {Log.d(TAG, "Commit callback delayed by " + (jitterNanos * 0.000001f)+ " ms which is more than twice the frame interval of "+ (mFrameIntervalNanos * 0.000001f) + " ms!  "+ "Setting frame time to " + (lastFrameOffset * 0.000001f)+ " ms in the past.");mDebugPrintNextFrameTimeDelta = true;}frameTimeNanos = now - lastFrameOffset;mLastFrameTimeNanos = frameTimeNanos;}}}try {// 迭代執(zhí)行隊列所有任務for (CallbackRecord c = callbacks; c != null; c = c.next) {// 回調(diào)CallbackRecord的run,其內(nèi)部回調(diào)Callback的runc.run(frameTimeNanos);}} finally {synchronized (mLock) {mCallbacksRunning = false;do {final CallbackRecord next = callbacks.next;//回收CallbackRecordrecycleCallbackLocked(callbacks);callbacks = next;} while (callbacks != null);}}}

主要內(nèi)容就是取對應任務類型的隊列,遍歷隊列執(zhí)行所有任務,執(zhí)行任務是 CallbackRecord的 run 方法:

    private static final class CallbackRecord {public CallbackRecord next;public long dueTime;public Object action; // Runnable or FrameCallbackpublic Object token;@UnsupportedAppUsagepublic void run(long frameTimeNanos) {if (token == FRAME_CALLBACK_TOKEN) {// 通過postFrameCallback 或 postFrameCallbackDelayed,會執(zhí)行這里((FrameCallback)action).doFrame(frameTimeNanos);} else {//取出Runnable執(zhí)行run()((Runnable)action).run();}}}

前面看到mChoreographer.postCallback傳的token是null,所以取出action,就是Runnable,執(zhí)行run(),這里的action就是 ViewRootImpl 發(fā)起的繪制任務mTraversalRunnable了,那么這樣整個邏輯就閉環(huán)了。
那么 啥時候 token == FRAME_CALLBACK_TOKEN 呢?答案是Choreographer的postFrameCallback()方法:

    public void postFrameCallback(FrameCallback callback) {postFrameCallbackDelayed(callback, 0);}public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {if (callback == null) {throw new IllegalArgumentException("callback must not be null");}//也是走到是postCallbackDelayedInternal,并且注意是CALLBACK_ANIMATION類型,//token是FRAME_CALLBACK_TOKEN,action就是FrameCallbackpostCallbackDelayedInternal(CALLBACK_ANIMATION,callback, FRAME_CALLBACK_TOKEN, delayMillis);}public interface FrameCallback {public void doFrame(long frameTimeNanos);}

可以看到postFrameCallback()傳入的是FrameCallback實例,接口FrameCallback只有一個doFrame()方法。并且也是走到postCallbackDelayedInternal,FrameCallback實例作為action傳入,token則是FRAME_CALLBACK_TOKEN,并且任務是CALLBACK_ANIMATION類型。

Choreographer的postFrameCallback()通常用來計算丟幀情況,使用方式如下:

		//Application.javapublic void onCreate() {super.onCreate();//在Application中使用postFrameCallbackChoreographer.getInstance().postFrameCallback(new FPSFrameCallback(System.nanoTime()));}public class FPSFrameCallback implements Choreographer.FrameCallback {private static final String TAG = "FPS_TEST";private long mLastFrameTimeNanos = 0;private long mFrameIntervalNanos;public FPSFrameCallback(long lastFrameTimeNanos) {mLastFrameTimeNanos = lastFrameTimeNanos;mFrameIntervalNanos = (long)(1000000000 / 60.0);}@Overridepublic void doFrame(long frameTimeNanos) {//初始化時間if (mLastFrameTimeNanos == 0) {mLastFrameTimeNanos = frameTimeNanos;}final long jitterNanos = frameTimeNanos - mLastFrameTimeNanos;if (jitterNanos >= mFrameIntervalNanos) {final long skippedFrames = jitterNanos / mFrameIntervalNanos;if(skippedFrames>30){//丟幀30以上打印日志Log.i(TAG, "Skipped " + skippedFrames + " frames!  "+ "The application may be doing too much work on its main thread.");}}mLastFrameTimeNanos=frameTimeNanos;//注冊下一幀回調(diào)Choreographer.getInstance().postFrameCallback(this);}}

Choreographer的postCallback()、postFrameCallback() 作用理解:發(fā)送任務 存隊列中,監(jiān)聽VSync信號,當前VSync到來時 會使用mHandler發(fā)送異步message,這個message的Runnable就是隊列中的所有任務。

7. 自定義View流程是什么

一般需要重寫onMeasure,onLayout和onDraw方法。

  • onMeasure 測量view的大小,用于確定 View 的測量寬/高。
    • Viewgroup先測量所有子View然后再測量自身
    • View直接測量自身
  • onLayout 布局View,用于確定 View 在父容器中的放置位置。
    • ViewGroup先確定自身的位置,再確定自子View的位置
    • View直接確定自身的位置
  • onDraw 繪制View
    • 一般ViewGroup不實現(xiàn)繪制,做容器作用
    • View實現(xiàn)一些特殊的繪制功能
  • 測量模式

MeasureSpec
MeasureSpec是View的一個公有靜態(tài)內(nèi)部類,它是一個 32 位的int值,高 2 位表示 SpecMode(測量模式),低 30 位表示 SpecSize(測量尺寸/測量大小)。
MeasureSpec將兩個數(shù)據(jù)打包到一個int值上,可以減少對象內(nèi)存分配,并且其提供了相應的工具方法可以很方便地讓我們從一個int值中抽取出 View 的 SpecMode 和 SpecSize。

  • UNSPECIFIED:表示父容器對子View 未施加任何限制,子View 尺寸想多大就多大。
  • EXACTLY:如果子View 的模式為EXACTLY,則表示子View 已設置了確切的測量尺寸,或者父容器已檢測出子View 所需要的確切大小。這種模式對應于LayoutParams.MATCH_PARENT和子View 設置具體數(shù)值兩種情況。
  • AT_MOST:表示自適應內(nèi)容,在該種模式下,View 的最大尺寸不能超過父容器的 SpecSize,因此也稱這種模式為 最大值模式。這種模式對應于LayoutParams.WRAP_CONTENT。
// frameworks/base/core/java/android/view/View.java
public static class MeasureSpec {private static final int MODE_SHIFT = 30;private static final int MODE_MASK  = 0x3 << MODE_SHIFT;/*** Measure specification mode: The parent has not imposed any constraint* on the child. It can be whatever size it wants.*/public static final int UNSPECIFIED = 0 << MODE_SHIFT;/*** Measure specification mode: The parent has determined an exact size* for the child. The child is going to be given those bounds regardless* of how big it wants to be.*/public static final int EXACTLY     = 1 << MODE_SHIFT;/*** Measure specification mode: The child can be as large as it wants up* to the specified size.*/public static final int AT_MOST     = 2 << MODE_SHIFT;// 生成測量規(guī)格public static int makeMeasureSpec(int size, int mode) {if (sUseBrokenMakeMeasureSpec) {return size + mode;} else {return (size & ~MODE_MASK) | (mode & MODE_MASK);}}// 獲取測量模式public static int getMode(int measureSpec) {return (measureSpec & MODE_MASK);}// 獲取測量大小public static int getSize(int measureSpec) {return (measureSpec & ~MODE_MASK);}...
}

LayoutParams
對 View 進行測量,最關(guān)鍵的一步就是計算得到 View 的MeasureSpec,子View 在創(chuàng)建時,可以指定不同的LayoutParams(布局參數(shù)),LayoutParams的源碼主要內(nèi)容如下所示:

// frameworks/base/core/java/android/view/ViewGroup.java
public static class LayoutParams {.../*** Special value for the height or width requested by a View.* MATCH_PARENT means that the view wants to be as big as its parent,* minus the parent's padding, if any. Introduced in API Level 8.*/public static final int MATCH_PARENT = -1;/*** Special value for the height or width requested by a View.* WRAP_CONTENT means that the view wants to be just large enough to fit* its own internal content, taking its own padding into account.*/public static final int WRAP_CONTENT = -2;/*** Information about how wide the view wants to be. Can be one of the* constants FILL_PARENT (replaced by MATCH_PARENT* in API Level 8) or WRAP_CONTENT, or an exact size.*/public int width;/*** Information about how tall the view wants to be. Can be one of the* constants FILL_PARENT (replaced by MATCH_PARENT* in API Level 8) or WRAP_CONTENT, or an exact size.*/public int height;...
}
  • LayoutParams.MATCH_PARENT:表示子View 的尺寸與父容器一樣大(注:需要減去父容器padding部分空間,讓父容器padding生效)
  • LayoutParams.WRAP_CONTENT:表示子View 的尺寸自適應其內(nèi)容大小(注:需要包含子View 本身的padding空間)
  • width/height:表示 View 的設置寬/高,即layout_width和layout_height設置的值,其值有三種選擇:LayoutParams.MATCH_PARENT、LayoutParams.WRAP_CONTENT和 具體數(shù)值。

LayoutParams會受到父容器的MeasureSpec的影響,測量過程會依據(jù)兩者之間的相互約束最終生成子View 的MeasureSpec,完成 View 的測量規(guī)格。
總之。View 的MeasureSpec受自身的LayoutParams和父容器的MeasureSpec共同決定(DecorView的MeasureSpec是由自身的LayoutParams和屏幕尺寸共同決定,參考后文)。也因此,如果要求取子View 的MeasureSpec,那么首先就需要知道父容器的MeasureSpec,層層逆推而上,即最終就是需要知道頂層View(即DecorView)的MeasureSpec,這樣才能一層層傳遞下來,這整個過程需要結(jié)合Activity的啟動過程進行分析。
總結(jié)
View 的繪制主要有以下一些核心內(nèi)容:
三大流程:View 繪制主要包含如下三大流程:

  • measure:測量流程,主要負責對 View 進行測量,其核心邏輯位于View#measure(…),真正的測量處理由View#onMeasure(…)負責。默認的測量規(guī)則為:
    • 如果 View 的布局參數(shù)為LayoutParams.WRAP_CONTENT或LayoutParams.MATCH_PARENT,那么其測量大小為 SpecSize;如果其布局參數(shù)為LayoutParams.UNSPECIFIED,那么其測量大小為android:minWidth/android:minHeight和其背景之間的較大值。
    • 自定義View 通常覆寫onMeasure(…)方法,在其內(nèi)一般會對WRAP_CONTENT預設一個默認值,區(qū)分WARP_CONTENT和MATCH_PARENT效果,最終完成自己的測量寬/高。而ViewGroup在onMeasure(…)方法中,通常都是先測量子View,收集到相應數(shù)據(jù)后,才能最終測量自己。
  • layout:布局流程,主要完成對 View 的位置放置,其核心邏輯位于View#layout(…),該方法內(nèi)部主要通過View#setFrame(…)記錄自己的四個頂點坐標(記錄與對應成員變量中即可),完成自己的位置放置,最后會回調(diào)View#onLayout(…)方法,在其內(nèi)完成對 子View 的布局放置。

注:不同于 measure 流程首先對 子View 進行測量,最后才測量自己,layout 流程首先是先定位自己的布局位置,然后才處理放置 子View 的布局位置。

  • draw:繪制流程,就是將 View 繪制到屏幕上,其核心邏輯位于View#draw(…),主要就是對 背景、自身內(nèi)容(onDraw(…))、子View(dispatchDraw(…))裝飾(滾動條、前景等) 進行繪制。

注:通常自定義View 覆寫onDraw(…)方法,完成自己的繪制即可,ViewGroup 一般充當容器使用,因此通常無需覆寫onDraw(…)。

  • Activity 的根視圖(即DecorView)最終是綁定到ViewRootImpl,具體是由ViewRootImpl#setView(…)進行綁定關(guān)聯(lián)的,后續(xù) View 繪制的三大流程都是均有ViewRootImpl負責執(zhí)行的。
  • 對 View 的測量流程中,最關(guān)鍵的一步是求取 View 的MeasureSpec,View 的MeasureSpec是在其父容器MeasureSpec的約束下,結(jié)合自己的LayoutParams共同測量得到的,具體的測量邏輯由ViewGroup#getChildMeasureSpec(…)負責。
    DecorView的MeasureSpec取決于自己的LayoutParams和屏幕尺寸,具體的測量邏輯位于ViewRootImpl#getRootMeasureSpec(…)。

總結(jié)一下 View 繪制的整個流程:

  • 首先,當 Activity 啟動時,會觸發(fā)調(diào)用到ActivityThread#handleResumeActivity(…),其內(nèi)部會經(jīng)歷一系列過程,生成DecorViewViewRootImpl等實例,最后通過**ViewRootImpl#setView(decor,MATCH_PARENT)**設置 Activity 根View。

注:ViewRootImpl#setView(…)內(nèi)容通過將其成員屬性ViewRootImpl#mView指向DecorView,完成兩者之間的關(guān)聯(lián)。

  • ViewRootImpl成功關(guān)聯(lián)DecorView后,其內(nèi)部會設置同步屏障并發(fā)送一個CALLBACK_TRAVERSAL異步渲染消息,在下一次 VSYNC 信號到來時,CALLBACK_TRAVERSAL就會得到響應,從而最終觸發(fā)執(zhí)行ViewRootImpl#performTraversals(…),真正開始執(zhí)行 View 繪制流程。
  • ViewRootImpl#performTraversals(…)內(nèi)部會依次調(diào)用ViewRootImpl#performMeasure(…)、**ViewRootImpl#performLayout(…)ViewRootImpl#performDraw(…)**三大繪制流程,其中:
    • performMeasure(…):內(nèi)部主要就是對DecorView執(zhí)行測量流程:DecorView#measure(…)。DecorView是一個FrameLayout,其布局特性是層疊布局,所占的空間就是其 子View 占比最大的寬/高,因此其測量邏輯(onMeasure(…))是先對所有 子View 進行測量,具體是通過ViewGroup#measureChildWithMargins(…)方法對 子View 進行測量,子View 測量完成后,記錄最大的寬/高,設置為自己的測量大小(通過View#setMeasuredDimension(…)),如此便完成了DecorView的測量流程。
    • performLayout(…):內(nèi)部其實就是調(diào)用DecorView#layout(…),如此便完成了DecorView的布局位置,最后會回調(diào)DecorView#onLayout(…),負責 子View 的布局放置,核心邏輯就是計算出各個 子View 的坐標位置,最后通過child.layout(…)完成 子View 布局。
    • performDraw():內(nèi)部最終調(diào)用到的是DecorView#draw(…),該方法內(nèi)部并未對繪制流程做任何修改,因此最終執(zhí)行的是View#draw(…),所以主要就是依次完成對DecorView的 背景、子View(dispatchDraw(…)) 和 視圖裝飾(滾動條、前景等) 的繪制。

8. 了解View事件分發(fā)機制嗎

事件分發(fā)機制主要靠三個方法完成dispatchTouchEventonInterceptTouchEventonTouchEvent

  • dispatchTouchEvent(event):用于進行點擊事件的分發(fā)。
  • onInterceptTouchEvent(event):用于進行點擊事件的攔截。
  • onTouchEvent(event):用于處理點擊事件。
    三個函數(shù)的參數(shù)均為even,即上面所說的3種類型的輸入事件,返回值均為boolean 類型
    三種方法調(diào)用關(guān)系可以用偽代碼表示:
    public boolean dispatchTouchEvent(MotionEvent ev) {boolean consume = false;//事件是否被消費if (onInterceptTouchEvent(ev)){//調(diào)用onInterceptTouchEvent判斷是否攔截事件consume = onTouchEvent(ev);//如果攔截則調(diào)用自身的onTouchEvent方法}else{consume = child.dispatchTouchEvent(ev);//不攔截調(diào)用子View的dispatchTouchEvent方法}return consume;//返回值表示事件是否被消費,true事件終止,false調(diào)用父View的onTouchEvent方法}

整個事件分發(fā)到處理可以用一個U形圖來表示
在這里插入圖片描述
簡述流程
從頂層DecorView開始分析

  • 點擊事件都通過dispatchTouchEvent分發(fā),因為是頂層沒有更上層的處理,因此不管返回true或者false都表示事件被消費,不再傳遞。返回super開始向下傳遞。
  • 交給子View的dispatchTouchEvent,此時因為有上層View。返回true表示事件被消費,返回false交給上層onTouchEvent處理。返回super繼續(xù)向下傳遞。因為是ViewGroup有onInterceptTouchEvent實現(xiàn)。onInterceptTouchEvent返回true表示需要攔截交給自身的onTouchEvent處理。返回false或者super則表示繼續(xù)向下傳遞。
  • 傳遞到View時dispatchTouchEvent返回true表示事件被消費false交給上層處理,因為沒有onInterceptTouchEvent實現(xiàn),所以super就是默認交給自己的onTouchEvent處理
  • onTouchEvent 返回true表示被消費不再傳遞,返回false就交給上層onTouchEvent處理

9. ListVie和RecycleView的區(qū)別

1. 優(yōu)化

ListView優(yōu)化需要自定義ViewHolder和判斷convertView是否為null。 而RecyclerView是存在規(guī)定好的ViewHolder。

2. 布局不同

對于ListView,只能在垂直的方向滾動。而對于RecyclerView,他里面的LayoutManager中制定了一套可以擴展的布局排列接口,所以我們可以重寫LayoutManager來定制自己需要的布局。RecycleView可以根據(jù)LayoutManger有橫向,瀑布和表格布局

3. 更新數(shù)據(jù)

recycleView可以支持在添加,刪除或者移動Item的時候,RecyclerView.ItemAnimator添加動畫效果,而listview不支持。而且RecyclerView有四重緩存,而ListView只有二重緩存。ListView和RecyclerView最大的區(qū)別在于數(shù)據(jù)源改變時的緩存的處理邏輯,ListView是"一鍋端",將所有的mActiveViews都移入了二級緩存mScrapViews,而RecyclerView則是更加靈活地對每個View修改
標志位,區(qū)分是否重新bindView。

4. 自定義適配器

ListView的適配器繼承ArrayAdapter;RecycleView的適配器繼承RecyclerAdapter,并將范類指定為子項對象類.ViewHolder(內(nèi)部類)。

5. 綁定事件不同

ListView是在主方法中ListView對象的setOnItemClickListener方法;RecyclerView則是在子項具體的View中去注冊事件。

10. 展開講講recycleView

recycleView以下幾個部分

  • LayoutManager 布局管理器,實現(xiàn)列表的布局效果
    • LinearLayoutManager 線性布局管理器
    • StaggeredGridLayoutManager 瀑布流布局管理器
    • GridLayoutManager 網(wǎng)格布局管理器
  • Adapter 適配器,適配數(shù)據(jù)如何展示
  • ItemDecoration Item 的裝飾器,經(jīng)常用來設置 Item 的分割線。
  • ItemAnimator:非必選項,設置 RV 中 Item 的動畫。

recycleView會將測量 onMeasure 和布局 onLayout 的工作委托給 LayoutManager 來執(zhí)行,不同的 LayoutManager 會有不同風格的布局顯示,這是一種策略模式。用一張圖來描述這段過程如下:
在這里插入圖片描述

recycleView的緩存了解嗎

recycleView分為四級緩存

  • 一級緩存 mAttachedScrap&mChangedScrap
    這兩者主要用來緩存屏幕內(nèi)的 ViewHolder。主要作用是數(shù)據(jù)更新時直接復用舊的ViewHolder。
    例如下拉刷新時,只需要在原有的 ViewHolder 基礎(chǔ)上進行重新綁定新的數(shù)據(jù) data 即可,而這些舊的 ViewHolder 就是被保存在 mAttachedScrap 和 mChangedScrap 中。實際上當我們調(diào)用 RV 的 notifyXXX 方法時,就會向這兩個列表進行填充,將舊 ViewHolder 緩存起來。

  • 二級緩存 mCachedViews
    它用來緩存移除屏幕之外的 ViewHolder,默認情況下緩存?zhèn)€數(shù)是 2個,不過可以通過 setViewCacheSize 方法來改變緩存的容量大小。如果 mCachedViews 的容量已滿,則會根據(jù) FIFO 的規(guī)則將舊 ViewHolder 拋棄,然后添加新的 ViewHolder,如下所示:
    在這里插入圖片描述

  • 三級緩存 ViewCacheExtension

  • 這是 RV 預留給開發(fā)人員的一個抽象類,在這個類中只有一個抽象方法,如下:

public abstract static class viewCacheExtension{@Nullablepublic abstract view getviewForPositionAndType(@NonNull Recycler recycler,int position,int type);
}

開發(fā)人員可以通過繼承 ViewCacheExtension,并復寫抽象方法 getViewForPositionAndType 來實現(xiàn)自己的緩存機制。只是一般情況下我們不會自己實現(xiàn)也不建議自己去添加緩存邏輯,因為這個類的使用門檻較高,需要開發(fā)人員對 RV 的源碼非常熟悉。

  • 四級緩存 RecycledViewPool
    RecycledViewPool 同樣是用來緩存屏幕外的 ViewHolder,當 mCachedViews 中的個數(shù)已滿(默認為 2),則從 mCachedViews 中淘汰出來的 ViewHolder 會先緩存到 RecycledViewPool 中。ViewHolder 在被緩存到 RecycledViewPool 時,會將內(nèi)部的數(shù)據(jù)清理,因此從 RecycledViewPool 中取出來的 ViewHolder 需要重新調(diào)用 onBindViewHolder 綁定數(shù)據(jù)。
    • RecycledViewPool 是根據(jù) type 來獲取 ViewHolder,每個 type 默認最大緩存 5 個
    • RecycledViewPool 是一個靜態(tài)類,因此是可以多個RecycleView共用一個RecycledViewPool 緩存的
    • 可以通過RecyclerView.getRecycledViewPool(). setMaxRecycledViews(int viewType, int max) 修改RecycledViewPool的緩存大小;

問題1. RecyclerView第一次layout時,會發(fā)生預布局pre-layout嗎?

第一次布局時,并不會觸發(fā)pre-layout。pre-layout只會在每次notify change時才會被觸發(fā),目的是通過saveOldPosition方法將屏幕中各位置上的ViewHolder的坐標記錄下來,并在重新布局之后,通過對比實現(xiàn)Item的動畫效果。

問題2. 如果自定義LayoutManager需要注意什么?

在RecyclerView的dispatchLayoutStep1階段,會調(diào)用自定義LayoutManager的 supportsPredictiveItemAnimations 方法判斷在某些狀態(tài)下是否展示predictive animation。以下LinearLayoutManager的實現(xiàn):

@Override
public boolean supportsPredictiveItemAnimations() {
return mPendingSavedState == null && mLastStackFromEnd == mStackFromEnd;
}

如果 supportsPredictiveItemAnimations 返回true,則LayoutManager中復寫onLayoutChildren方法會被調(diào)用2次:一次是在pre-layout,另一次是real-layout。

因為會有pre-layout和real-layout,所有在自定義LayoutManager中,需要根據(jù)RecyclerView.State中的isPreLayout方法的返回值,在這兩次布局中做區(qū)分。比如LinearLayoutManager中的onLayoutChildren中有如下判斷:

 @Overridepublic void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {......if (state.isPreLayout() && mPendingScrollPosition != RecyclerView.NO_POSITION&& mPendingScrollPositionOffset != INVALID_OFFSET) {// if the child is visible and we are going to move it around, we should layout// extra items in the opposite direction to make sure new items animate nicely// instead of just fading infinal View existing = findViewByPosition(mPendingScrollPosition);if (existing != null) {final int current;final int upcomingOffset;if (mShouldReverseLayout) {current = mOrientationHelper.getEndAfterPadding()- mOrientationHelper.getDecoratedEnd(existing);upcomingOffset = current - mPendingScrollPositionOffset;} else {current = mOrientationHelper.getDecoratedStart(existing)- mOrientationHelper.getStartAfterPadding();upcomingOffset = mPendingScrollPositionOffset - current;}if (upcomingOffset > 0) {extraForStart += upcomingOffset;} else {extraForEnd -= upcomingOffset;}}}......}

意思就是如果當前正在update的item是可見狀態(tài),則需要在pre-layout階段額外填充一個item,目的是為了保證處于不可見狀態(tài)的item可以平滑的滑動到屏幕內(nèi)。
在這里插入圖片描述
![在這里插入圖片描述](https://img-blog.csdnimg.cn/9bba1e8565d54db182c78f0c271d1f9b.png在這里插入圖片描述
如果自定義LayoutManager并沒有實現(xiàn)pre-layout,或者實現(xiàn)不合理,則當item2移出屏幕時,只會將item3和item4進行平滑移動,而item5只是單純的appear到屏幕中,如下所示:
在這里插入圖片描述

問題3. CachedView和RecycledViewPool兩者區(qū)別

緩存到CachedView中的ViewHolder并不會清理相關(guān)信息(比如position、state等),因此剛移出屏幕的ViewHolder,再次被移回屏幕時,只要從CachedView中查找并顯示即可,不需要重新綁定(bindViewHolder)。

而緩存到RecycledViewPool中的ViewHolder會被清理狀態(tài)和位置信息,因此從RecycledViewPool查找到ViewHolder,需要重新調(diào)用bindViewHolder綁定數(shù)據(jù)。

問題4. 你是從哪些方面優(yōu)化RecyclerView的?

  • 盡量將復雜的數(shù)據(jù)處理操作放到異步中完成。RecyclerView需要展示的數(shù)據(jù)經(jīng)常是從遠端服務器上請求獲取,但是在網(wǎng)絡請求拿到數(shù)據(jù)之后,需要將數(shù)據(jù)做扁平化操作,盡量將最優(yōu)質(zhì)的數(shù)據(jù)格式返回給UI線程。優(yōu)化RecyclerView的布局,避免將其與ConstraintLayout使用
  • 針對快速滑動事件,可以使用addOnScrollListener添加對快速滑動的監(jiān)聽,當用戶快速滑動時,停止加載數(shù)據(jù)操作。
  • 如果ItemView的高度固定,可以使用setHasFixSize(true)。這樣RecyclerView在onMeasure階段可以直接計算出高度,不需要多次計算子ItemView的高度,這種情況對于垂直RecyclerView中嵌套橫向RecyclerView效果非常顯著。
  • 當UI是Tab feed流時,可以考慮使用RecycledViewPool來實現(xiàn)多個RecyclerView的緩存共享。

11. 你知道IPC嗎?

IPC 是Inter-Process Communication的縮寫,含義為進程間通信或者跨進程通信,是指兩個進程之間進行數(shù)據(jù)交換的過程。
Android中的IPC方式主要有以下幾種:

  • 使用Bundle
    四大組件中的三大組件(Activity、Service、Receiver)都是支持在Intent中傳遞Bundle數(shù)據(jù)的,由于Bundle實現(xiàn)了Parcelable接口,所以它可以方便地在不同的進程間傳輸,這是一種最簡單的進程間通信方式,

  • 使用文件共享
    文件共享方式適合在對數(shù)據(jù)同步要求不高的進程之間進行通信,并且要妥善處理并發(fā)讀/寫的問題。

  • 使用Messenger

    • Messenger可以翻譯為信使,顧名思義,通過它可以在不同進程中傳遞Message對象,在Message中放入我們需要傳遞的數(shù)據(jù),就可以輕松地實現(xiàn)數(shù)據(jù)的進程間傳遞了。
    • Messenger是一種輕量級的IPC方案,它的底層實現(xiàn)是AIDL,從構(gòu)造方法的實現(xiàn)上我們可以明顯看出AIDL的痕跡,不管是IMessenger還是Stub.asInterface,這種使用方法都表明它的底層是AIDL。

    實現(xiàn)一個Messenger有如下幾個步驟,分為服務端和客戶端:
    1.服務端進程
    首先,我們需要在服務端創(chuàng)建一個Service來處理客戶端的連接請求,同時創(chuàng)建一個Handler并通過它來創(chuàng)建一個Messenger對象,然后在Service的onBind中返回這個Messenger對象底層的Binder即可
    2.客戶端進程
    客戶端進程中,首先要綁定服務端的Service,綁定成功后用服務端返回的IBinder對象創(chuàng)建一個Messenger,通過這個Messenger就可以向服務端發(fā)送消息了,發(fā)消息類型為Message對象。如果需要服務端能夠回應客戶端,就和服務端一樣,我們還需要創(chuàng)建一個Handler并創(chuàng)建一個新的Messenger,并把這個Messenger對象通過Message的replyTo參數(shù)傳遞給服務端,服務端通過這個replyTo參數(shù)就可以回應客戶端。

  • 使用AIDL

    • 1.服務端服務端首先要創(chuàng)建一個Service用來監(jiān)聽客戶端的連接請求,然后創(chuàng)建一個AIDL文件,將暴露給客戶端的接口在這個AIDL文件中聲明,最后在Service中實現(xiàn)這個AIDL接口即可。
    • 2.客戶端客戶端所要做事情就稍微簡單一些,首先需要綁定服務端的Service,綁定成功后,將服務端返回的Binder對象轉(zhuǎn)成AIDL接口所屬的類型,接著就可以調(diào)用AIDL中的方法了。
    • 基本數(shù)據(jù)類型(int、long、char、boolean、double等);
    • String和CharSequence;
    • List:只支持ArrayList,里面每個元素都必須能夠被AIDL支持;
    • Map:只支持HashMap,里面的每個元素都必須被AIDL支持,包括key和value;
    • Parcelable:所有實現(xiàn)了Parcelable接口的對象;
    • AIDL:所有的AIDL接口本身也可以在AIDL文件中使用。
  • 使用ContentProvider
    ContentProvider是Android中提供的專門用于不同應用間進行數(shù)據(jù)共享的方式。和Messenger一樣,ContentProvider的底層實現(xiàn)同樣也是Binder。雖然ContentProvider的底層實現(xiàn)是Binder,但是它的使用過程要比AIDL簡單許多,這是因為系統(tǒng)已經(jīng)為我們做了封裝,使得我們無須關(guān)心底層細節(jié)即可輕松實現(xiàn)IPC。

  • 使用Socket
    Socket也稱為“套接字”,是網(wǎng)絡通信中的概念,它分為流式套接字和用戶數(shù)據(jù)報套接字兩種,分別對應于網(wǎng)絡的傳輸控制層中的TCP和UDP協(xié)議。
    TCP協(xié)議是面向連接的協(xié)議,提供穩(wěn)定的雙向通信功能,TCP連接的建立需要經(jīng)過“三次握手”才能完成,為了提供穩(wěn)定的數(shù)據(jù)傳輸功能,其本身提供了超時重傳機制,因此具有很高的穩(wěn)定性;
    而UDP是無連接的,提供不穩(wěn)定的單向通信功能,當然UDP也可以實現(xiàn)雙向通信功能。在性能上,UDP具有更好的效率,其缺點是不保證數(shù)據(jù)一定能夠正確傳輸,尤其是在網(wǎng)絡擁塞的情況下。
    在這里插入圖片描述

12. 展開說說Binder

Binder 是一種進程間通信機制,基于開源的 OpenBinder 實現(xiàn)。優(yōu)點是高性能穩(wěn)定性安全性。

  • 性能
    • Socket 作為一款通用接口,其傳輸效率低,開銷大,主要用在跨網(wǎng)絡的進程間通信本機上進程間的低速通信。
    • 消息隊列管道采用存儲-轉(zhuǎn)發(fā)方式,即數(shù)據(jù)先從發(fā)送方緩存區(qū)拷貝到內(nèi)核開辟的緩存區(qū)中,然后再從內(nèi)核緩存區(qū)拷貝到接收方緩存區(qū),至少有兩次拷貝過程。共享內(nèi)存雖然無需拷貝,但控制復雜,難以使用。
    • Binder 只需要一次數(shù)據(jù)拷貝,性能上僅次于共享內(nèi)存。
    • 共享內(nèi)存 不需要拷貝數(shù)據(jù),但是使用麻煩。
  • 穩(wěn)定性
    Binder 基于 C/S 架構(gòu),客戶端(Client)有什么需求就丟給服務端(Server)去完成,架構(gòu)清晰、職責明確又相互獨立穩(wěn)定性更好。
  • 安全性
    Android 為每個安裝好的 APP 分配了自己的 UID,故而進程的 UID 是鑒別進程身份的重要標志。傳統(tǒng)的 IPC 只能由用戶在數(shù)據(jù)包中填入 UID/PID,但這樣不可靠,容易被惡意程序利用。可靠的身份標識只有由 IPC 機制在內(nèi)核中添加。其次傳統(tǒng)的 IPC 訪問接入點是開放的,只要知道這些接入點的程序都可以和對端建立連接,不管怎樣都無法阻止惡意程序通過猜測接收方地址獲得連接。同時 Binder 既支持實名 Binder,又支持匿名 Binder,安全性高。

問題1. Binder實現(xiàn)原理是什么

在這里插入圖片描述

  • Linux系統(tǒng)將進程空間劃分為用戶空間內(nèi)核空間
  • 進程與進程之間是相互隔離的,進程間需要進行數(shù)據(jù)交互就需要采用IPC

傳統(tǒng) IPC 通信原理

  1. 消息發(fā)送方將要發(fā)送的數(shù)據(jù)存放在內(nèi)存緩存區(qū)中,通過系統(tǒng)調(diào)用進入內(nèi)核態(tài)
  2. 然后內(nèi)核程序在內(nèi)核空間分配內(nèi)存,開辟一塊內(nèi)核緩存區(qū),調(diào)用 copyfromuser() 函數(shù)將數(shù)據(jù)從用戶空間的內(nèi)存緩存區(qū)拷貝到內(nèi)核空間的內(nèi)核緩存區(qū)中。
  3. 同樣的,接收方進程在接收數(shù)據(jù)時在自己的用戶空間開辟一塊內(nèi)存緩存區(qū),然后內(nèi)核程序調(diào)用 copytouser() 函數(shù)將數(shù)據(jù)從內(nèi)核緩存區(qū)拷貝到接收進程的內(nèi)存緩存區(qū)。這樣數(shù)據(jù)發(fā)送方進程和數(shù)據(jù)接收方進程就完成了一次數(shù)據(jù)傳輸,我們稱完成了一次進程間通信。

這種傳統(tǒng)的 IPC 通信方式有兩個問題

  1. 性能低下,一次數(shù)據(jù)傳遞需要經(jīng)歷:內(nèi)存緩存區(qū) --> 內(nèi)核緩存區(qū) --> 內(nèi)存緩存區(qū),需要 2 次數(shù)據(jù)拷貝
  2. 接收數(shù)據(jù)的緩存區(qū)由數(shù)據(jù)接收進程提供,但是接收進程并不知道需要多大的空間來存放將要傳遞過來的數(shù)據(jù),因此只能開辟盡可能大的內(nèi)存空間或者先調(diào)用 API 接收消息頭來獲取消息體的大小,這兩種做法不是浪費空間就是浪費時間。

Binder 跨進程通信原理
跨進程通信是需要內(nèi)核空間做支持的。傳統(tǒng)的 IPC 機制如管道、Socket 都是內(nèi)核的一部分,可以通過內(nèi)核支持來實現(xiàn)進程間通信。但是 Binder 并不是 Linux 系統(tǒng)內(nèi)核的一部分。如何實現(xiàn)呢?利用了Linux系統(tǒng)的動態(tài)內(nèi)核可加載模塊內(nèi)存映射

  • 動態(tài)內(nèi)核可加載模塊(Loadable Kernel Module,LKM)機制
    模塊是具有獨立功能的程序,它可以被單獨編譯,但是不能獨立運行。它在運行時被鏈接到內(nèi)核作為內(nèi)核的一部分運行。這樣,Android 系統(tǒng)通過動態(tài)添加一個內(nèi)核模塊運行在內(nèi)核空間,用戶進程之間通過這個內(nèi)核模塊作為橋梁來實現(xiàn)通信。也就是Binder 驅(qū)動(Binder Dirver)
  • 內(nèi)存映射
    內(nèi)存映射通過 mmap() 來實現(xiàn),mmap() 是操作系統(tǒng)中一種內(nèi)存映射的方法。內(nèi)存映射簡單的講就是將用戶空間的一塊內(nèi)存區(qū)域映射到內(nèi)核空間。映射關(guān)系建立后,用戶對這塊內(nèi)存區(qū)域的修改可以直接反應到內(nèi)核空間;反之內(nèi)核空間對這段區(qū)域的修改也能直接反應到用戶空間。

一次完整的 Binder IPC 通信過程通常是這樣:

  1. 首先 Binder 驅(qū)動內(nèi)核空間創(chuàng)建一個數(shù)據(jù)接收緩存區(qū)
  2. 接著在內(nèi)核空間開辟一塊內(nèi)核緩存區(qū),建立內(nèi)核緩存區(qū)內(nèi)核中數(shù)據(jù)接收緩存區(qū)之間的映射關(guān)系,以及內(nèi)核中數(shù)據(jù)接收緩存區(qū)接收進程用戶空間地址的映射關(guān)系;
  3. 發(fā)送方進程通過系統(tǒng)調(diào)用 copyfromuser() 將數(shù)據(jù) copy 到內(nèi)核中的內(nèi)核緩存區(qū),由于內(nèi)核緩存區(qū)和接收進程的用戶空間存在內(nèi)存映射,因此也就相當于把數(shù)據(jù)發(fā)送到了接收進程的用戶空間,這樣便完成了一次進程間的通信。
    在這里插入圖片描述

13. 了解MVC,MVP,MVVM嗎?

  • MVC是 模型 - 視圖 - 控制器 MVC的目的就是將M和V的代碼分離,且MVC是單向通信,必須通過Controller來承上啟下。

    • Model模型層,數(shù)據(jù)模型及其業(yè)務邏輯,是針對業(yè)務模型建立的數(shù)據(jù)結(jié)構(gòu),Model與View無關(guān),而與業(yè)務有關(guān)。
    • View視圖層,用于與用戶實現(xiàn)交互的頁面,通常實現(xiàn)數(shù)據(jù)的輸入和輸出功能。
    • controller控制器,用于連接Model層和View層,完成Model層和View層的交互。還可以處理頁面業(yè)務邏輯,它接收并處理來自用戶的請求,并將Model返回給用戶。
      在這里插入圖片描述
  • MVP是 模型 - 視圖 - 表示器

    • Model:模型層,用于數(shù)據(jù)存儲以及業(yè)務邏輯。
    • View:視圖層,用于展示與用戶實現(xiàn)交互的頁面,通常實現(xiàn)數(shù)據(jù)的輸入和輸出功能。
    • Presenter:表示器,用于連接M層、V層,完成Model層與View層的交互,還可以進行業(yè)務邏輯的處理。
      在這里插入圖片描述
  • MVVM是 模型 - 視圖 - 視圖模型 MVVM與MVP框架區(qū)別在于:MVVM采用雙向綁定:View的變動,自動反映在ViewModel,反之亦然。
    Model:數(shù)據(jù)模型(數(shù)據(jù)處理業(yè)務),指的是后端傳遞的數(shù)據(jù)。
    View:視圖,將Model的數(shù)據(jù)以某種方式展示出來。
    ViewModel:視圖模型,數(shù)據(jù)的雙向綁定(當Model中的數(shù)據(jù)發(fā)生改變時View就感知到,當View中的數(shù)據(jù)發(fā)生變化時Model也能感知到),是MVVM模式的核心。ViewModel 層把 Model 層和 View 層的數(shù)據(jù)同步自動化了,解決了 MVP 框架中數(shù)據(jù)同步比較麻煩的問題,不僅減輕了 ViewModel 層的壓力,同時使得數(shù)據(jù)處理更加方便——只需告訴 View 層展示的數(shù)據(jù)是 Model 層中的哪一部分即可。
    在這里插入圖片描述

14. 使用過jetpack庫嗎?

1. LifeCycle原理

Lifecycle是一個管理生命周期的工具類,Lifecycle是一個抽象類,它通過Event枚舉類型維護了生命周期分別對應的狀態(tài),通過State維護了執(zhí)行了某一個生命周期函數(shù)后和在執(zhí)行下一個生命周期函數(shù)前被觀察者(Activity或Fragment)處于什么狀態(tài)

問題 1. LifeCycle怎么做到監(jiān)聽LifecycleOwner(Activity或Fragment)的生命周期的?

Lifecycle是通過在Activity中綁定了一個空的fragment來實現(xiàn)監(jiān)聽Activity的生命周期的,因為如果在Activity中添加一個fragment 那在fragment的生命周期執(zhí)行的時候 就能知道宿主對應的生命周期執(zhí)行了
ComponentActivityonCreate方法中添加一個名字為ReportFragment空Fragment

ComponentActivity@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);mSavedStateRegistryController.performRestore(savedInstanceState);ReportFragment.injectIfNeededIn(this);if (mContentLayoutId != 0) {setContentView(mContentLayoutId);}}
    @Overridepublic void onActivityCreated(Bundle savedInstanceState) {super.onActivityCreated(savedInstanceState);dispatchCreate(mProcessListener);dispatch(Lifecycle.Event.ON_CREATE);}@Overridepublic void onStart() {super.onStart();dispatchStart(mProcessListener);dispatch(Lifecycle.Event.ON_START);}@Overridepublic void onResume() {super.onResume();dispatchResume(mProcessListener);dispatch(Lifecycle.Event.ON_RESUME);}@Overridepublic void onPause() {super.onPause();dispatch(Lifecycle.Event.ON_PAUSE);}@Overridepublic void onStop() {super.onStop();dispatch(Lifecycle.Event.ON_STOP);}@Overridepublic void onDestroy() {super.onDestroy();dispatch(Lifecycle.Event.ON_DESTROY);// just want to be sure that we won't leak reference to an activitymProcessListener = null;}private void dispatch(Lifecycle.Event event) {Activity activity = getActivity();if (activity instanceof LifecycleRegistryOwner) {((LifecycleRegistryOwner) activity).getLifecycle().handleLifecycleEvent(event);return;}if (activity instanceof LifecycleOwner) {Lifecycle lifecycle = ((LifecycleOwner) activity).getLifecycle();if (lifecycle instanceof LifecycleRegistry) {((LifecycleRegistry) lifecycle).handleLifecycleEvent(event);}}}
  • ComponentActivity 實現(xiàn)了 LifecycleOwner ,所以可以添加觀察者,并且自己的onCreate 方法中,創(chuàng)建并添加了一個ReportFragment ,用來感應自身的生命周期的回調(diào), 并進行對應分發(fā)。

  • 在分發(fā)過程中,Lifecycle 通過內(nèi)部維護的狀態(tài)機 將生命周期事件轉(zhuǎn)化為State狀態(tài),并進行存儲,在 分發(fā)過程中,通過狀態(tài)比較來判斷 當前過程是正在可見還是正在不可見,不同過程采用不同策略。最后通過調(diào)用 LifecycleEventObserver 的 onStateChanged 方法 來進行回調(diào)。

2. Room

Room 是 Google 官方推出的數(shù)據(jù)庫 ORM 框架。ORM:即 Object Relational Mapping,即對象關(guān)系映射為面向?qū)ο蟮恼Z言。
Room 包含三個組件:Entity、DAO 和 Database。

  • Entity:實體類,數(shù)據(jù)庫中的表(table),保存數(shù)據(jù)庫并作為應用持久性數(shù)據(jù)底層連接的主要訪問點。
  • DAO:數(shù)據(jù)庫訪問對象,提供訪問 DB 的 API,如增刪改查等方法。
  • Database:訪問底層數(shù)據(jù)庫的入口,管理著真正的數(shù)據(jù)庫文件。

原理分析
Room 在編譯期通過 kapt 處理 @Dao@Database 注解,通過注解在運行時生成代碼和SQL語句,并生成 DAO 和 Database 的實現(xiàn)類:TestRoomDataBase_Impl 和 UserDao_Impl。

  • createFromAsset()/createFromFile():從 SD 卡或者 Assets 的 DB 文件創(chuàng)建 RoomDatabase 實例
  • addMigrations():添加一個數(shù)據(jù)庫遷移(migration),當進行數(shù)據(jù)版本升級時需要
  • allowMainThreadQueries():允許在 UI 線程進行數(shù)據(jù)庫查詢,默認是不允許的
  • fallbackToDestructiveMigration():如果找不到 migration 則重建數(shù)據(jù)庫表(會造成數(shù)據(jù)丟失)

3. LiveData

LiveData 是 Jetpack 推出的基于觀察者的消息訂閱/分發(fā)的可觀察數(shù)據(jù)組件,具有宿主(Activity、Fragment)生命周期感知能力,這種感知能力可確保 LiveData 僅分發(fā)消息給處于活躍狀態(tài)的觀察者,即只有處于活躍狀態(tài)的觀察者才能收到消息。
事件注冊流程

  • 首先會將Observer與其宿主包裝成一個WrapObserver,繼承自LifecycleObserver
  • 宿主將WrapObserver注冊到Lifecycle監(jiān)聽自身狀態(tài),此時會觸發(fā)Lifecycle的事件回調(diào)
  • 判斷監(jiān)聽的宿主當前狀態(tài),是否已經(jīng)銷毀,如果是的話則進行反注冊
  • 如果不是,則進行至少一次的狀態(tài)對齊,如果當前監(jiān)聽的宿主是活躍的則繼而觸發(fā)事件分發(fā)邏輯
  • 如果版本號不一致,則進行觸發(fā)監(jiān)聽器,同步數(shù)據(jù)源(粘性事件)

事件分發(fā)流程
postValue自帶切換主線程的能力

public abstract class LiveData<T> {static final Object NOT_SET = new Object();//用于線程切換時,暫存Data用的變量volatile Object mPendingData = NOT_SET;protected void postValue(T value) {boolean postTask;synchronized (mDataLock) {//1.判斷是否為一次有效的postValuepostTask = mPendingData == NOT_SET;//2.暫存對象到mPendingData中mPendingData = value;}//3.過濾無效postValueif (!postTask) {return;}//4.這里通過ArchTaskExecutor切換線程,其實ArchTaskExecutor切換線程的核心靠一個掌握著MainLooper的Handler切換ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);}//用于給Handler執(zhí)行切換線程的Runnableprivate final Runnable mPostValueRunnable = new Runnable() {@SuppressWarnings("unchecked")@Overridepublic void run() {Object newValue;synchronized (mDataLock) {//5.從緩存中獲得DatanewValue = mPendingData;mPendingData = NOT_SET;}//6.通過setValue設置數(shù)據(jù)setValue((T) newValue);}};
}

setValue(T data)
事件分發(fā)的關(guān)鍵函數(shù),主要做了三件事
1、版本+1,LiveData的mVersion變量表示的是發(fā)送數(shù)據(jù)的版本,每次發(fā)送一次數(shù)據(jù), 它都會+1;
2、將值賦值給全局變量mData;
3、調(diào)用dispatchingValue方法分發(fā)數(shù)據(jù),dispatchingValue方法中主要調(diào)用的是considerNotify方法

   @MainThreadprotected void setValue(T value) {//1.判斷當前是否主線程,如果不是則報錯(因為多線程會導致數(shù)據(jù)問題)assertMainThread("setValue");//2.版本號自增mVersion++;//3.數(shù)據(jù)源更新mData = value;//4.分發(fā)事件邏輯dispatchingValue(null);}    //事件分發(fā)的函數(shù)@SuppressWarnings("WeakerAccess") /* synthetic access */void dispatchingValue(@Nullable ObserverWrapper initiator) {//5.判斷是否分發(fā)中,如果是的話,忽略這次分發(fā)if (mDispatchingValue) {mDispatchInvalidated = true;return;}//6.設置表示mDispatchingValue = true;do {mDispatchInvalidated = false;//7.判斷參數(shù)中,有沒指定Observer,如果有則只通知指定Observer,沒有的話則遍歷全部Observer通知if (initiator != null) {//7.0 considerNotify(initiator);initiator = null;} else {//7.1 遍歷全部Observer通知更新for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {considerNotify(iterator.next().getValue());if (mDispatchInvalidated) {break;}}}} while (mDispatchInvalidated);mDispatchingValue = false;}  //分發(fā)函數(shù)private void considerNotify(ObserverWrapper observer) {//8. 判斷如果當前監(jiān)聽器不活躍,則不分發(fā)if (!observer.mActive) {return;}//9. 二次確認監(jiān)聽器所在宿主是否活躍,如果不活躍,則證明Observer中的mActive狀態(tài)并非最新的,調(diào)用activeStateChanged更新狀態(tài)if (!observer.shouldBeActive()) {observer.activeStateChanged(false);return;}//10. 判斷版本號是否一致,如果一致則不需要分發(fā)if (observer.mLastVersion >= mVersion) {return;}//11. 對齊版本號observer.mLastVersion = mVersion;//12. 通知監(jiān)聽器observer.mObserver.onChanged((T) mData);}

observe和observeForever區(qū)別

observer會自動監(jiān)聽生命周期,當是STARTED和RESUMED的時候才會觸發(fā)訂閱,而observeForever則是會一直檢測數(shù)據(jù)變化,只要數(shù)據(jù)變化了就會觸發(fā)訂閱。

@MainThread
public void observeForever(@NonNull Observer<? super T> observer) {assertMainThread("observeForever");// 會創(chuàng)建一個總是活動的觀察者LiveData<T>.AlwaysActiveObserver wrapper = new LiveData.AlwaysActiveObserver(observer);......

如源碼所示,observeForever會創(chuàng)建一個總是活動的觀察者AlwaysActiveObserver,改類繼承ObserverWrapper,并實現(xiàn)了shouldBeActive方法,而且始終返回true,所以只要數(shù)據(jù)一改變,會觸發(fā)onChanged方法。

private class AlwaysActiveObserver extends LiveData<T>.ObserverWrapper {AlwaysActiveObserver(Observer<? super T> observer) {super(observer);}// 一直返回trueboolean shouldBeActive() {return true;}
}

LiveData粘性事件和數(shù)據(jù)倒灌

  • 粘性事件:先setValue/postValue,后調(diào)用observe(),如果成功收到了回調(diào),即為粘性事件。
  • 數(shù)據(jù)倒灌:先setValue/postValue,后調(diào)用observe(new Obs()),至此收到了回調(diào)。然后再第二次調(diào)用observe(new anotherObs()),如果還能收到第一次的回調(diào),則為“數(shù)據(jù)倒灌”。

4. DataBinding

15. launcher頁面點擊app圖標啟動過程了解嗎?以點擊淘寶為例。

  1. 點擊桌面launcher的淘寶圖標時,會調(diào)用桌面程序的onClick方法,調(diào)用startActivity方法啟動app
  2. 啟動新app,屬于跨進程啟動。跨進程通信用到AMS(activity Manager Service),ActivityManagerNative.getDefault返回ActivityManagerService的遠程接口,即ActivityManagerProxy接口,
    通過Binder驅(qū)動程序,ActivityManagerProxy與AMS服務通信,則實現(xiàn)了跨進程到System進程。AMS響應Launcher進程。在AMS的onTransact方法里面會獲取到請求的Activity
  3. 處理啟動的activity的參數(shù),判斷是否需要新建task啟動Activity
  4. ApplicationThread對象的遠程接口,通過調(diào)用這個遠程接口的schedulePauseActivity,通知launcher進程進入Paused狀態(tài),
    此時AMS對Launcher的請求已經(jīng)響應,這時我們又通過Binder通信回調(diào)至Launcher進程。
    Launcher進程掛起Launcher,再次通知AMS。
  5. AMS知道了Launcher已經(jīng)掛起之后,就可以放心的為新的Activity準備啟動工作了,首先,APP肯定需要一個新的進程去運行,所以需要創(chuàng)建一個新進程,AMS通過Socket去和Zygote協(xié)商,如果需要創(chuàng)建進程,那么就會fork自身,創(chuàng)建一個進程,新的進程會導入ActivityThread類,這就是每一個應用程序都有一個ActivityThread與之對應的原因;
  6. 啟動目標進程
    創(chuàng)建新進程的時候,AMS會保存一個ProcessRecord信息,如果應用程序中的AndroidManifest.xml配置文件中,我們沒有指定Application標簽的process屬性,系統(tǒng)就會默認使用package的名稱。每一個應用程序都有自己的uid,因此,這里uid + process的組合就可以為每一個應用程序創(chuàng)建一個ProcessRecord。
    新的進程會導入android.app.ActivityThread類,并且執(zhí)行它的main函數(shù)
  7. 綁定新進程
    在AMS中注冊應用進程,啟動棧頂頁面
    此時在App進程,經(jīng)過一些列的調(diào)用鏈最終調(diào)用至MainActivity:onCreate函數(shù),之后會調(diào)用至onResume,而后會通知AMS該MainActivity已經(jīng)處于resume狀態(tài)。至此,整個啟動流程完成。

流程分析

16. 為什么用到socket又用到Binder,為什么不統(tǒng)一用binder呢?

  1. 先后時序問題
    安卓中一般使用的binder引用,都是保存在ServiceManager進程中的,而如果想從ServiceManager中獲取到對應的binder引用,前提是需要注冊,而注冊的行為是在對應的邏輯代碼執(zhí)行時才會去注冊的。
    流程上,是Init產(chǎn)生Zygote進程和ServiceManager進程,然后Zygote進程產(chǎn)生SystemServer進程。如果AMS想通過binder向Zygote發(fā)送信號,必須向ServiceManager獲取Zygote的binder引用,而前提是Zygote進程中必須提前注冊好才行。
    而實際上,Init進程是先創(chuàng)建ServiceManager,后創(chuàng)建Zygote進程的。雖然Zygote更晚創(chuàng)建,但是并不能保證Zygote進程去注冊binder的時候,ServiceManager已經(jīng)初始化好了,因為兩者屬于兩個進程,是并行的。
  2. 多線程問題
    Linux中,fork進程其實并不是完美的fork,linux設計之初只考慮到了主線程的fork,也就是說如果主進程中存在子線程,那么fork進程中,其子線程的鎖狀態(tài),掛起狀態(tài)等等都是不可恢復的,只有主進程的才可以恢復。
    而binder作為典型的CS模式,其在Server是通過線程來實現(xiàn)的,Server等待請求狀態(tài)時,必然是處于一種掛起的狀態(tài)。所以如果使用binder機制,zygote進程fork子進程后,子進程的binder的Server永遠處于一種掛起不可恢復的狀態(tài),這樣的設計無疑是非常差的。
    所以,zygote如果使用binder,會導致子進程中binder線程的掛起和鎖狀態(tài)不可恢復,
  3. 效率問題
    Binder基于mmap機制,只會進行一次拷貝,所以效率是很高的。那么Socket就一定低嗎?
    其實答案也許會出乎你的意料。如果這個問題你問GPT,GPT會告訴你,localsocket的效率是高于binder的。其原因,是雖然binder只有一次拷貝,比socket的兩次更少,但是拷貝次數(shù)只是一個很重要的原因,但并不是所有影響的因素。binder因為涉及到安全驗證等等環(huán)節(jié),所以實際上效率反而沒有l(wèi)ocalsocket高。據(jù)說騰訊小程序跨進程通訊使用的就是LocalSocket的方式。
    所以,LocalSocket效率其實并不低。
  4. 安全問題 普遍認為Socket是不安全的,因為它缺乏PID校驗,所以導致任何進程都可以訪問。但是LocalSocket是安全的。
    代碼如下,我們嘗試直接給zygote的進程發(fā)送socket消息,看zygote進程是否可以接受。如果可以接受的話,那么我們只要發(fā)送指定格式的字符串,APP也能啟動其它應用進程了。相關(guān)代碼是直接拷貝AMS中的。
 val localSocket = LocalSocket()val localSocketAddress =LocalSocketAddress("zygote", LocalSocketAddress.Namespace.RESERVED)localSocket.connect(localSocketAddress)val outputStream = localSocket.outputStreamoutputStream.write(1)outputStream.flush()

實驗下來LocalSocket也是有權(quán)限驗證的。
在這里插入圖片描述
connectLocal在native層的實現(xiàn)是socket_connect_local方法。

socket_connect_local(JNIEnv *env, jobject object,jobject fileDescriptor, jstring name, jint namespaceId)
{int ret;int fd; if (name == NULL) {jniThrowNullPointerException(env, NULL);return;} fd = jniGetFDFromFileDescriptor(env, fileDescriptor); if (env->ExceptionCheck()) {return;} ScopedUtfChars nameUtf8(env, name);ret = socket_local_client_connect(fd,nameUtf8.c_str(),namespaceId,SOCK_STREAM);if (ret < 0) {jniThrowIOException(env, errno);return;}
}

所以最終的校驗邏輯應該在Linux層的socket_local_client_connect方法。

socket_connect_local(JNIEnv *env, jobject object,jobject fileDescriptor, jstring name, jint namespaceId)
{int ret;int fd;if (name == NULL) {jniThrowNullPointerException(env, NULL);return;} fd = jniGetFDFromFileDescriptor(env, fileDescriptor);if (env->ExceptionCheck()) {return;} ScopedUtfChars nameUtf8(env, name); ret = socket_local_client_connect(fd,nameUtf8.c_str(),namespaceId,SOCK_STREAM); if (ret < 0) {jniThrowIOException(env, errno);return;}
}

LocalSocket其實也有權(quán)限校驗,并不意味著可以被所有進程隨意調(diào)用。
5. Binder拷貝問題
進程的fork,是拷貝一個和原進程一摸一樣的進程,其中的各種內(nèi)存對象自然也會被拷貝。所以用來接收消息去fork進程的binder對象自然也會被拷貝。但是這個拷貝對于APP層有用嗎?那自然是沒用的,所以就憑白多占用了一塊無用的內(nèi)存區(qū)域。
說到這你自然想問,如果通過socket的方式,不也平白無故的多占用一塊Socket內(nèi)存區(qū)域嗎?是的,確實是,但是fork出APP進程之后,APP進程會去主動的關(guān)閉掉這個socket,從而釋放這塊區(qū)域。相關(guān)代碼在ZygoteConnection的processCommand方法中:

try {if (pid == 0) {// in childzygoteServer.setForkChild(); zygoteServer.closeServerSocket();IoUtils.closeQuietly(serverPipeFd);serverPipeFd = null; return handleChildProc(parsedArgs, childPipeFd,parsedArgs.mStartChildZygote);} else {// In the parent. A pid < 0 indicates a failure and will be handled in// handleParentProc.IoUtils.closeQuietly(childPipeFd);childPipeFd = null;handleParentProc(pid, serverPipeFd);return null;}} 

17. context 和 activity的區(qū)別

在這里插入圖片描述

Context是個抽象類,通過類的結(jié)構(gòu)可以知道:Activity、Service、Application都是Context的子類;從Android系統(tǒng)的角度來理解:Context是一個場景,描述的是一個應用程序環(huán)境的信息,即上下文,代表與操作系統(tǒng)的交互的一種過程。
Activity和Application都是Context的子類,他們維護的生命周期不一樣。前者維護一個Acitivity的生命周期,所以其對應的Context也只能訪問該activity內(nèi)的各種資源。后者則是維護一個Application的生命周期。

  • Activity繼承自ContextThemeWrapper類其內(nèi)部包含了與主題相關(guān)的接口。主題就是清單文件中android:theme為Application或Activity元素指定的主題。(Activity才需要主題,Serviceu不需要,因為服務是沒有界面的后臺場景,所以服務直接繼承ContextWrapper。Application同理。)

18. 一個應用程序中有多少個context?

看完以上分析答案顯而易見:總Context實例個數(shù) = Service個數(shù) + Activity個數(shù) + 1(Application對應的Context實例)

開源框架篇

1. OKHTTP了解嗎?

OkHttp 是一套處理 HTTP 網(wǎng)絡請求的開源框架

OkHttpclient client new OkHttpclient();
Requestrequest new Request.Builder()
.url(url)
.build();
client.newcall(request).enqueue(new Callback(){
@Override
public void onFailure(Call call,IOException e){}
@Override
public void onResponse(Call call,Response response) throws IOException{};
  1. 一般使用異步請求enqueue(),會交給內(nèi)部RealCall實際上是RealCall.enqueue()
  2. 然后會交給調(diào)度器Dispatcher執(zhí)行入隊方法,最終請求操作是委托給 Dispatcher的enqueue 方法內(nèi)實現(xiàn)的
  • Dispatcher 是 OkHttpClient 的調(diào)度器,是一種門戶模式。主要用來實現(xiàn)執(zhí)行、取消異步請求操作。本質(zhì)上是內(nèi)部維護了一個線程池去執(zhí)行異步操作,
    并且在 Dispatcher 內(nèi)部根據(jù)一定的策略,保證最大并發(fā)個數(shù)、同一 host 主機允許執(zhí)行請求的線程個數(shù)等。
synchronized void enqueue(AsyncCall call){
if (runningAsyncCalls.size()<maxRequests &runningCallsForHost(call)<maxRequestsPerHost){
runningAsyncCalls.add(call);
executorService().execute(call);
}else{readyAsyncCalls.add(call);}
}

線程池執(zhí)行了一個 AsyncCall,而 AsyncCall 實現(xiàn)了 Runnable 接口,因此整個操作會在一個子線程(非 UI 線程)中執(zhí)行。
AsyncCall 中的 run

final class AsyncCall extends NamedRunnable {
......@Override protected void execute() {boolean signalledCallback = false;try {Response response = getResponseWithInterceptorChain();if (retryAndFollowUpInterceptor.isCanceled()) {signalledCallback = true;responseCallback.onFailure(RealCall.this, new IOException("Canceled"));} else {signalledCallback = true;responseCallback.onResponse(RealCall.this, response);}} catch (IOException e) {if (signalledCallback) {// Do not signal the callback twice!Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);} else {responseCallback.onFailure(RealCall.this, e);}} finally {client.dispatcher().finished(this);}}}

最后response是通過getResponseWithInterceptorChain()中獲取的,內(nèi)部是一個攔截器的調(diào)用鏈

Response getResponseWithInterceptorChain() throws IOException {// Build a full stack of interceptors.List<Interceptor> interceptors = new ArrayList<>();interceptors.addAll(client.interceptors());interceptors.add(retryAndFollowUpInterceptor);interceptors.add(new BridgeInterceptor(client.cookieJar()));interceptors.add(new CacheInterceptor(client.internalCache()));interceptors.add(new ConnectInterceptor(client));if (!forWebSocket) {interceptors.addAll(client.networkInterceptors());}interceptors.add(new CallServerInterceptor(forWebSocket));Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0, originalRequest);return chain.proceed(originalRequest);}

問題 1. 知道OkHttp有幾個攔截器以及作用嗎嗎?

八大攔截器

  • client.interceptors() 自定義攔截器,開發(fā)者設置的,會按照開發(fā)者的要求,在所有的攔截器處理之前進行最早的攔截處理,比如一些公共參數(shù),Header都可以在這里添加。
  • retryAndFollowUpInterceptor 重試攔截器,這里會對連接做一些初始化工作,以及請求失敗的重試工作,重定向的后續(xù)請求工作。跟他的名字一樣,就是做重試工作還有一些連接跟蹤工作
  • BridgeInterceptor 橋接連接器。主要是進行請求前的一些操作,將我們的請求設置成服務器能識別的請求,比如設置一系列頭部信息,比如設置請求內(nèi)容長度,編碼,gzip壓縮,cookie等。
  • CacheInterceptor:緩存攔截器。作用是緩存請求和響應,比如同一個請求之前發(fā)生過,這次就不需要重新構(gòu)建了,直接從緩存取;響應同理
  • ConnectInterceptor:連接攔截器,為當前請求找到一個合適的連接,比如如果從連接池中可以找到能復用的連接,就不要再創(chuàng)建新的連接了。這里主要就是負責建立連接了,會建立TCP連接或者TLS連接,以及負責編碼解碼的HttpCodec
  • networkInterceptors 網(wǎng)絡攔截器。這里也是開發(fā)者自己設置的,所以本質(zhì)上和第一個攔截器差不多,但是由于位置不同,所以用處也不同。這個位置添加的攔截器可以看到請求和響應的數(shù)據(jù)了,所以可以做一些網(wǎng)絡調(diào)試。
  • CallServerInterceptor:連接服務器攔截器,負責向服務器發(fā)送真正的請求,接受服務器的響應

問題 2. OkHttp怎么實現(xiàn)連接池

為什么需要連接池?
頻繁的進行建立Sokcet連接和斷開Socket是非常消耗網(wǎng)絡資源和浪費時間的,所以HTTP中的keepalive連接對于降低延遲和提升速度有非常重要的作用。keepalive機制是什么呢?也就是可以在一次TCP連接中可以持續(xù)發(fā)送多份數(shù)據(jù)而不會斷開連接。所以連接的多次使用,也就是復用就變得格外重要了,而復用連接就需要對連接進行管理,于是就有了連接池的概念。

OkHttp中使用ConectionPool實現(xiàn)連接池,默認支持5個并發(fā)KeepAlive,默認鏈路生命為5分鐘。

怎么實現(xiàn)的?

  1. 首先,ConectionPool中維護了一個雙端隊列Deque,也就是兩端都可以進出的隊列,用來存儲連接。
  2. 然后在ConnectInterceptor,也就是負責建立連接的攔截器中,首先會找可用連接,也就是從連接池中去獲取連接,具體的就是會調(diào)用到ConectionPool的get方法。也就是遍歷了雙端隊列,如果連接有效,就會調(diào)用acquire方法計數(shù)并返回這個連接。
RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {assert (Thread.holdsLock(this));for (RealConnection connection : connections) {if (connection.isEligible(address, route)) {streamAllocation.acquire(connection, true);return connection;}}return null;}
  1. 如果沒找到可用連接,就會創(chuàng)建新連接,并會把這個建立的連接加入到雙端隊列中,同時開始運行線程池中的線程,其實就是調(diào)用了ConectionPool的put方法。
public final class ConnectionPool {void put(RealConnection connection) {if (!cleanupRunning) {//沒有連接的時候調(diào)用cleanupRunning = true;executor.execute(cleanupRunnable);}connections.add(connection);}
}
  1. ConectionPool有一個線程,是用來清理連接的,也就是cleanupRunnable
private final Runnable cleanupRunnable = new Runnable() {@Overridepublic void run() {while (true) {//執(zhí)行清理,并返回下次需要清理的時間。long waitNanos = cleanup(System.nanoTime());if (waitNanos == -1) return;if (waitNanos > 0) {long waitMillis = waitNanos / 1000000L;waitNanos -= (waitMillis * 1000000L);synchronized (ConnectionPool.this) {//在timeout時間內(nèi)釋放鎖try {ConnectionPool.this.wait(waitMillis, (int) waitNanos);} catch (InterruptedException ignored) {}}}}}};

也就是當如果空閑連接maxIdleConnections超過5個或者keepalive時間大于5分鐘,則將該連接清理掉。

long cleanup(long now) {synchronized (this) {//遍歷連接for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {RealConnection connection = i.next(); //檢查連接是否是空閑狀態(tài),//不是,則inUseConnectionCount + 1//是 ,則idleConnectionCount + 1if (pruneAndGetAllocationCount(connection, now) > 0) {inUseConnectionCount++;continue;} idleConnectionCount++; // If the connection is ready to be evicted, we're done.long idleDurationNs = now - connection.idleAtNanos;if (idleDurationNs > longestIdleDurationNs) {longestIdleDurationNs = idleDurationNs;longestIdleConnection = connection;}}//如果超過keepAliveDurationNs或maxIdleConnections,//從雙端隊列connections中移除if (longestIdleDurationNs >= this.keepAliveDurationNs|| idleConnectionCount > this.maxIdleConnections) {      connections.remove(longestIdleConnection);} else if (idleConnectionCount > 0) {      //如果空閑連接次數(shù)>0,返回將要到期的時間// A connection will be ready to evict soon.return keepAliveDurationNs - longestIdleDurationNs;} else if (inUseConnectionCount > 0) {// 連接依然在使用中,返回保持連接的周期5分鐘return keepAliveDurationNs;} else {// No connections, idle or in use.cleanupRunning = false;return -1;}}closeQuietly(longestIdleConnection.socket());// Cleanup again immediately.return 0;}

怎樣屬于空閑連接?

  public void acquire(RealConnection connection, boolean reportedAcquired) {assert (Thread.holdsLock(connectionPool));if (this.connection != null) throw new IllegalStateException();this.connection = connection;this.reportedAcquired = reportedAcquired;connection.allocations.add(new StreamAllocationReference(this, callStackTrace));}

RealConnection中,有一個StreamAllocation虛引用列表allocations。每創(chuàng)建一個連接,就會把連接對應的StreamAllocationReference添加進該列表中,如果連接關(guān)閉以后就將該對象移除。
其實可以這樣理解,在上層反復調(diào)用acquire和release函數(shù),來增加或減少connection.allocations所維持的集合的大小,到最后如果size大于0,則代表RealConnection還在使用連接,如果size等于0,那就說明已經(jīng)處于空閑狀態(tài)了

問題 3. 簡述一下OkHttp的一個工作流程

  • 異步請求會交給調(diào)度器Dispatcher入隊,
    • Dispatcher內(nèi)部維護了一個線程池ExecutorService和三個隊列 readyAsyncCallsrunningAsyncCallsrunningSyncCalls
    • readyAsyncCalls 待請求隊列,里面存儲準備執(zhí)行的異步請求。
    • runningAsyncCalls 異步請求隊列,里面存儲正在執(zhí)行,包含已經(jīng)取消但是還沒有結(jié)束的請求。
    • runningSyncCalls 同步請求隊列,正在執(zhí)行的請求,包含已經(jīng)取消但是還沒有結(jié)束的請求。
  • 判斷正在進行異步請求隊列長度是否小于64,且單個Host正在執(zhí)行的請求數(shù)小于5;如果滿足,將請求添加到runningAsyncCalls隊列,同時使用內(nèi)部線程池執(zhí)行該請求
  • 如果條件不滿足就將請求添加到readyAsyncCalls隊列
  • 然后調(diào)用 promoteAndExecute(),從readyAsyncCalls中取出符合條件的請求到runningAsyncCalls,并執(zhí)行。
  • 調(diào)用攔截器鏈拿到服務器響應

同步請求

  • 首先出現(xiàn)一個同步代碼塊,對當前對象加鎖,通過一個標志位executed判斷該對象的execute方法是否已經(jīng)執(zhí)行過,如果執(zhí)行過就拋出異常;這也就是同一個Call只能執(zhí)行一次的原因
  • 第二步:調(diào)用Dispatcher的executed方法,將請求放入分發(fā)器,這是非常重要的一步
  • 第三步:通過攔截器連獲取返回結(jié)果Response
  • 第四步:調(diào)用dispatcher的finished方法,回收同步請求

問題 4. Okhttp 如何實現(xiàn)緩存功能?它是如何根據(jù) Cache-Control 首部來判斷緩存策略的?

  • Okhttp 實現(xiàn)緩存功能是通過 CacheInterceptor 攔截器和 Cache 類來實現(xiàn)的。
    在創(chuàng)建 OkHttpClient 對象時,可以指定一個 Cache 對象,用于存儲緩存的響應。
    CacheInterceptor 攔截器會根據(jù)請求和響應的 Cache-Control 首部來判斷是否使用緩存或網(wǎng)絡,以及更新緩存。
    Cache-Control 首部是用于控制緩存行為的指令,它有以下幾種常見的值:

    • no-cache:表示不使用本地緩存,必須向服務器驗證緩存是否有效。
    • no-store:表示不使用本地緩存,也不更新本地緩存。
    • only-if-cached:表示只使用本地緩存,不使用網(wǎng)絡。
    • max-age=n:表示本地緩存在 n 秒內(nèi)有效,超過 n 秒后需要向服務器驗證或重新獲取。
    • must-revalidate:表示本地緩存必須向服務器驗證是否有效,如果無效則重新獲取。

? CacheInterceptor 攔截器的工作流程大致如下:

  • 根據(jù)請求查找是否有匹配的緩存響應,如果沒有,則直接使用網(wǎng)絡,并將響應寫入緩存(如果滿足條件)。
  • 如果有匹配的緩存響應,判斷是否過期或需要再驗證,如果是,則向服務器發(fā)送帶有驗證首部的請求,并根據(jù)服務器的響應來決定是否使用緩存或更新緩存。
  • 如果不過期或不需要再驗證,則直接使用緩存,并添加 Age 首部來表示緩存的新鮮度。

問題 5. Okhttp 如何自定義攔截器?你有沒有使用過或編寫過自己的攔截器?

通過實現(xiàn) Interceptor 接口,Interceptor 接口只有一個方法 intercept,該方法接收一個 Chain 參數(shù),表示攔截器鏈。在 intercept 方法中,可以對請求或響應進行修改或轉(zhuǎn)發(fā),并且可以決定是否繼續(xù)傳遞給下一個攔截器。在創(chuàng)建 OkHttpClient 對象時,可以通過 addInterceptoraddNetworkInterceptor 方法來添加自定義的應用攔截器或網(wǎng)絡攔截器。

我有使用過或編寫過自己的攔截器

  • 一個日志攔截器,用于打印請求和響應的信息,方便調(diào)試和監(jiān)控。
  • 一個加密攔截器,用于對請求參數(shù)進行加密,保證數(shù)據(jù)的安全性。
  • 一個認證攔截器,用于對請求添加認證信息,如 token、簽名等,實現(xiàn)用戶的身份驗證。

問題 6. Okhttp 如何管理連接池和線程池?它是如何復用和回收連接的?

  • Okhttp 管理連接池和線程池是通過 ConnectionPool 類和 Dispatcher 類來實現(xiàn)的。

    • ConnectionPool 類表示一個連接池,它維護了一個雙端隊列,用于存儲空閑的連接。
      它有一個清理線程,用于定期檢查連接是否過期或超過最大空閑數(shù),并將其移除。

    • Dispatcher 類表示一個調(diào)度器,它維護了三個雙端隊列,分別用于存儲同步任務、異步任務等待執(zhí)行的異步任務
      有一個線程池,用于執(zhí)行異步任務并根據(jù)最大并發(fā)數(shù)和主機數(shù)來調(diào)度任務執(zhí)行。

  • Okhttp 復用回收連接是通過 StreamAllocation 類和 RealConnection 類來實現(xiàn)的。

    • StreamAllocation 類表示一個流分配器,它負責管理連接的分配和釋放。
    • RouteSelector 對象,用于尋找最優(yōu)的路由和地址。
    • RealConnection 對象,用于表示當前分配的連接
    • release 方法,用于釋放連接,并根據(jù)連接是否空閑或是否可以復用來決定是否將其加入到連接池中。

RealConnection 類表示一個真實的連接,它封裝了一個 Socket 對象,用于建立 TCP 連接,并通過 Okio 獲取輸入流和輸出流。
allocationLimit 屬性,用于表示該連接可以分配給多少個流。
noNewStreams 屬性,用于表示該連接是否可以創(chuàng)建新的流。
onStreamFinished 方法,用于在流結(jié)束時減少 allocationLimit 的值,并根據(jù)情況釋放或回收連接。

2. Glide了解嗎?

Glide是Google推薦的一套快速高效的圖片加載框架功能強大且使用方便。
優(yōu)點

  • 使用方便,API簡潔。with、load、into三步就可以加載圖片
  • 生命周期自動綁定,根據(jù)綁定的Activity或者Fragment生命周期管理圖片請求
  • 高效的緩存策略,三級緩存策略
    • A. 支持Memory和Disk圖片緩存
    • B. Picasso 只會緩存原始尺寸的圖片,而 Glide 緩存的是多種規(guī)格,也就意味著 Glide 會根據(jù)你 ImageView 的大小來緩存相應大小的圖片尺寸.
    • C. 內(nèi)存開銷小
      默認的 Bitmap 格式是 RGB_565 格式,而 Picasso 默認的是 ARGB_8888 格式,這個內(nèi)存開銷要小一半。

Glide生命周期,**with()**方法就是用于綁定生命周期,

  • 傳入Application參數(shù) Glide生命周期和應用程序的生命周期同步。
  • 傳入非Application參數(shù) 不管傳入的是Activity、FragmentActivityFragment 最終的流程都是一樣的,那就是會向當前的Activity當中添加一個隱藏的Fragment,用于同步生命周期
  • 如果我們是在非主線程當中使用的Glide,那么不管你是傳入的Activity還是Fragment,都會被強制當成Application來處理。

Glide圖片緩存

  • Glide的緩存功能設計成 二級緩存:內(nèi)存緩存 & 硬盤緩存 緩存讀取順序:內(nèi)存緩存 --> 磁盤緩存 --> 網(wǎng)絡
    • 內(nèi)存緩存:防止應用重復將圖片數(shù)據(jù) 讀取到內(nèi)存當中。內(nèi)存緩存又分為2種,弱引用和Lrucache
      • 弱引用:弱引用的對象具備更短生命周期,因為當JVM進行垃圾回收時,一旦發(fā)現(xiàn)弱引用對象,都會進行回收(無論內(nèi)存充足否)
      • LruCache算法原理:將 最近使用的對象用強引用的方式存儲在LinkedHashMap中 ;當緩存滿時 ,將最近最少使用的對象從內(nèi)存中移除
    • 硬盤緩存:防止應用重復從網(wǎng)絡或其他地方重復下載和讀取數(shù)據(jù)。磁盤緩存就是DiskLrucache

在這里插入圖片描述
寫入磁盤緩存

  • Glide將圖片寫入磁盤緩存的時機:獲取圖片資源后 、圖片加載完成前
  • 寫入磁盤緩存又分為:將 原始圖片 寫入或?qū)?轉(zhuǎn)換后的圖片寫入磁盤緩存

寫入內(nèi)存緩存
Glide 將圖片寫入 內(nèi)存緩存的時機:圖片加載完成后 、圖片顯示出來前

  • 當 acquired 變量 >0 時,說明圖片正在使用,即該圖片緩存繼續(xù)存放到activeResources弱引用緩存中

  • 當 acquired變量 = 0,即說明圖片已經(jīng)不再被使用,就將該圖片的緩存Key從 activeResources弱引用緩存中移除,并存放到LruResourceCache緩存中實現(xiàn)了:

    • 正在使用中的圖片 采用 弱引用 的內(nèi)存緩存
    • 不在使用中的圖片 采用 LruCache算法 的內(nèi)存緩存

Glide5大磁盤緩存策略

  • DiskCacheStrategy.DATA: 只緩存原始圖片;
  • DiskCacheStrategy.RESOURCE: 只緩存轉(zhuǎn)換過后的圖片;
  • DiskCacheStrategy.ALL: 既緩存原始圖片,也緩存轉(zhuǎn)換過后的圖片;對于遠程圖片,緩存 DATA和 RESOURCE;對于本地圖片,只緩存 RESOURCE;
  • DiskCacheStrategy.NONE:不緩存任何內(nèi)容;
  • DiskCacheStrategy.AUTOMATIC:默認策略,嘗試對本地和遠程圖片使用最佳的策略。當下載網(wǎng)絡圖片時,使用DATA;對于本地圖片,使用RESOURCE;

3. EventBus了解嗎?

EventBus是一個開源庫,是用于Android開發(fā)的 “事件發(fā)布—訂閱總線”, 用來進行模塊間通信、解藕。它可以使用很少的代碼,來實現(xiàn)多組件之間的通信。
在這里插入圖片描述
EventBus的優(yōu)勢
1,簡化組件之間的通訊方式
2,對通信雙方進行解藕
3,使用ThreadMode靈活切換工作線程
5,庫比較小,不占內(nèi)存
EentBus缺點
1、使用的時候有定義很多event類。
2、event在注冊的時候會調(diào)用反射去遍歷注冊對象的方法在其中找出帶有@subscriber標簽的方法,性能不高。
3、需要自己注冊和反注冊,如果忘了反注冊就會導致內(nèi)存泄漏。
4、造成代碼邏輯不清晰,出現(xiàn)bug不容易溯源

  • ThreadMode.POSTING,默認的線程模式,在那個線程發(fā)送事件就在對應線程處理事件,避免了線程切換,效率高。
  • ThreadMode.MAIN,如在主線程(UI線程)發(fā)送事件,則直接在主線程處理事件;如果在子線程發(fā)送事件,則先將事件入隊列,然后通過 Handler 切換到主線程,依次處理事件。
  • ThreadMode.MAIN_ORDERED,無論在那個線程發(fā)送事件,都先將事件入隊列,然后通過 Handler 切換到主線程,依次處理事件。
  • ThreadMode.BACKGROUND,如果在主線程發(fā)送事件,則先將事件入隊列,然后通過線程池依次處理事件;如果在子線程發(fā)送事件,則直接在發(fā)送事件的線程處理事件。
  • ThreadMode.ASYNC,無論在那個線程發(fā)送事件,都將事件入隊列,然后通過線程池處理。

流程

  • 注冊
EventBus.getDefault().register(this);

register()方法主要分為查找注冊兩部分,
查找 的過程 findSubscriberMethods() 先從緩存中查找,如果找到則直接返回,否則去做下一步的查找過程,然后緩存查找到的集合然后調(diào)用findUsingInfo() 會在當前要注冊的類以及其父類中查找訂閱事件的方法,這里出現(xiàn)了一個FindState類,它是SubscriberMethodFinder的內(nèi)部類,用來輔助查找訂閱事件的方法,具體的查找過程在findUsingReflectionInSingleClass()方法,它主要通過反射查找訂閱事件的方法
注冊 subscribe()方法主要是得到了subscriptionsByEventType、typesBySubscriber兩個 HashMap。我們在發(fā)送事件的時候要用到subscriptionsByEventType,完成事件的處理。當取消 EventBus 注冊的時候要用到typesBySubscriber、subscriptionsByEventType,完成相關(guān)資源的釋放。
取消注冊

EventBus.getDefault().unregister(this);
public synchronized void unregister(Object subscriber) {// 得到當前注冊類對象 對應的 訂閱事件方法的參數(shù)類型 的集合List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);if (subscribedTypes != null) {// 遍歷參數(shù)類型集合,釋放之前緩存的當前類中的Subscriptionfor (Class<?> eventType : subscribedTypes) {unsubscribeByEventType(subscriber, eventType);}// 刪除以subscriber為key的鍵值對typesBySubscriber.remove(subscriber);} else {logger.log(Level.WARNING, "Subscriber to unregister was not registered before: " + subscriber.getClass());}}private void unsubscribeByEventType(Object subscriber, Class<?> eventType) {// 得到當前參數(shù)類型對應的Subscription集合List<Subscription> subscriptions = subscriptionsByEventType.get(eventType);if (subscriptions != null) {int size = subscriptions.size();// 遍歷Subscription集合for (int i = 0; i < size; i++) {Subscription subscription = subscriptions.get(i);// 如果當前subscription對象對應的注冊類對象 和 要取消注冊的注冊類對象相同,則刪除當前subscription對象if (subscription.subscriber == subscriber) {subscription.active = false;subscriptions.remove(i);i--;size--;}}}}

unregister()方法中,釋放了typesBySubscriber、subscriptionsByEventType中緩存的資源。
發(fā)送事件

EventBus.getDefault().post("Hello World!")public void post(Object event) {// currentPostingThreadState是一個PostingThreadState類型的ThreadLocal// PostingThreadState類保存了事件隊列和線程模式等信息PostingThreadState postingState = currentPostingThreadState.get();List<Object> eventQueue = postingState.eventQueue;// 將要發(fā)送的事件添加到事件隊列eventQueue.add(event);// isPosting默認為falseif (!postingState.isPosting) {// 是否為主線程postingState.isMainThread = isMainThread();postingState.isPosting = true;if (postingState.canceled) {throw new EventBusException("Internal error. Abort state was not reset");}try {// 遍歷事件隊列while (!eventQueue.isEmpty()) {// 發(fā)送單個事件// eventQueue.remove(0),從事件隊列移除事件postSingleEvent(eventQueue.remove(0), postingState);}} finally {postingState.isPosting = false;postingState.isMainThread = false;}}}

所以 post() 方法先將發(fā)送的事件保存到事件隊列,然后通過循環(huán)出隊列,將事件交給postSingleEvent()處理

private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {Class<?> eventClass = event.getClass();boolean subscriptionFound = false;// eventInheritance默認為true,表示是否向上查找事件的父類if (eventInheritance) {// 查找當前事件類型的Class,連同當前事件類型的Class保存到集合List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);int countTypes = eventTypes.size();// 遍歷Class集合,繼續(xù)處理事件for (int h = 0; h < countTypes; h++) {Class<?> clazz = eventTypes.get(h);subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);}} else {subscriptionFound = postSingleEventForEventType(event, postingState, eventClass);}if (!subscriptionFound) {if (logNoSubscriberMessages) {logger.log(Level.FINE, "No subscribers registered for event " + eventClass);}if (sendNoSubscriberEvent && eventClass != NoSubscriberEvent.class &&eventClass != SubscriberExceptionEvent.class) {post(new NoSubscriberEvent(this, event));}}}

postSingleEvent()方法中,根據(jù)eventInheritance屬性,決定是否向上遍歷事件的父類型,然后用postSingleEventForEventType() 方法進一步處理事件:

private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {CopyOnWriteArrayList<Subscription> subscriptions;synchronized (this) {// 獲取事件類型對應的Subscription集合subscriptions = subscriptionsByEventType.get(eventClass);}// 如果已訂閱了對應類型的事件if (subscriptions != null && !subscriptions.isEmpty()) {for (Subscription subscription : subscriptions) {// 記錄事件postingState.event = event;// 記錄對應的subscriptionpostingState.subscription = subscription;boolean aborted = false;try {// 最終的事件處理postToSubscription(subscription, event, postingState.isMainThread);aborted = postingState.canceled;} finally {postingState.event = null;postingState.subscription = null;postingState.canceled = false;}if (aborted) {break;}}return true;}return false;}

postSingleEventForEventType() 方法核心就是遍歷發(fā)送的事件類型對應的Subscription集合,然后調(diào)用postToSubscription() 方法處理事件。
處理事件
postToSubscription() 內(nèi)部會根據(jù)訂閱事件方法的線程模式,間接或直接的以發(fā)送的事件為參數(shù),通過反射執(zhí)行訂閱事件的方法。

private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {// 判斷訂閱事件方法的線程模式switch (subscription.subscriberMethod.threadMode) {// 默認的線程模式,在那個線程發(fā)送事件就在那個線程處理事件case POSTING:invokeSubscriber(subscription, event);break;// 在主線程處理事件case MAIN:// 如果在主線程發(fā)送事件,則直接在主線程通過反射處理事件if (isMainThread) {invokeSubscriber(subscription, event);} else {// 如果是在子線程發(fā)送事件,則將事件入隊列,通過Handler切換到主線程執(zhí)行處理事件// mainThreadPoster 不為空mainThreadPoster.enqueue(subscription, event);}break;// 無論在那個線程發(fā)送事件,都先將事件入隊列,然后通過 Handler 切換到主線程,依次處理事件。// mainThreadPoster 不為空case MAIN_ORDERED:if (mainThreadPoster != null) {mainThreadPoster.enqueue(subscription, event);} else {invokeSubscriber(subscription, event);}break;case BACKGROUND:// 如果在主線程發(fā)送事件,則先將事件入隊列,然后通過線程池依次處理事件if (isMainThread) {backgroundPoster.enqueue(subscription, event);} else {// 如果在子線程發(fā)送事件,則直接在發(fā)送事件的線程通過反射處理事件invokeSubscriber(subscription, event);}break;// 無論在那個線程發(fā)送事件,都將事件入隊列,然后通過線程池處理。case ASYNC:asyncPoster.enqueue(subscription, event);break;default:throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);}}

postToSubscription() 方法就是根據(jù)訂閱事件方法的線程模式、以及發(fā)送事件的線程來判斷如何處理事件,至于處理方式主要有兩種:
一種是在相應線程直接通過invokeSubscriber()方法,用反射來執(zhí)行訂閱事件的方法,這樣發(fā)送出去的事件就被訂閱者接收并做相應處理了:

void invokeSubscriber(Subscription subscription, Object event) {try {subscription.subscriberMethod.method.invoke(subscription.subscriber, event);} catch (InvocationTargetException e) {handleSubscriberException(subscription, event, e.getCause());} catch (IllegalAccessException e) {throw new IllegalStateException("Unexpected exception", e);}}

另外一種是先將事件入隊列(其實底層是一個List),然后做進一步處理,以**mainThreadPoster.enqueue(subscription, event)**為例簡單的分析下,其中mainThreadPoster是HandlerPoster類的一個實例

public class HandlerPoster extends Handler implements Poster {private final PendingPostQueue queue;private boolean handlerActive;......public void enqueue(Subscription subscription, Object event) {// 用subscription和event封裝一個PendingPost對象PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);synchronized (this) {// 入隊列queue.enqueue(pendingPost);if (!handlerActive) {handlerActive = true;// 發(fā)送開始處理事件的消息,handleMessage()方法將被執(zhí)行,完成從子線程到主線程的切換if (!sendMessage(obtainMessage())) {throw new EventBusException("Could not send handler message");}}}}@Overridepublic void handleMessage(Message msg) {boolean rescheduled = false;try {long started = SystemClock.uptimeMillis();// 死循環(huán)遍歷隊列while (true) {// 出隊列PendingPost pendingPost = queue.poll();......// 進一步處理pendingPosteventBus.invokeSubscriber(pendingPost);......}} finally {handlerActive = rescheduled;}}
}

HandlerPoster的enqueue()方法主要就是將subscription、event對象封裝成一個PendingPost對象,然后保存到隊列里,之后通過Handler切換到主線程,在handleMessage()方法將中將PendingPost對象循環(huán)出隊列,交給invokeSubscriber()方法進一步處理:

void invokeSubscriber(PendingPost pendingPost) {Object event = pendingPost.event;Subscription subscription = pendingPost.subscription;// 釋放pendingPost引用的資源PendingPost.releasePendingPost(pendingPost);if (subscription.active) {// 用反射來執(zhí)行訂閱事件的方法invokeSubscriber(subscription, event);}}

pendingPost中取出之前保存的eventsubscription,然后用反射來執(zhí)行訂閱事件的方法,又回到了第一種處理方式。所以mainThreadPoster.enqueue(subscription, event)的核心就是先將將事件入隊列,然后通過Handler從子線程切換到主線程中去處理事件。

在這里插入圖片描述
在這里插入圖片描述
在這里插入圖片描述

架構(gòu)方面

1. 組件化

一、優(yōu)點

  1. 基礎(chǔ)功能復用,節(jié)省開發(fā)時間
    在項目初期框架搭建的時候,基礎(chǔ)功能可直接搬移復用,日積月累,每個人/公司應該都會有一套自己的Base。
  2. 業(yè)務拆分,便于分工,實現(xiàn)解耦
    單獨的業(yè)務模塊抽取成一個獨立的Module,不同人員在各自的模塊實現(xiàn)自己的業(yè)務代碼,實現(xiàn)業(yè)務拆解、人員拆解,從而實現(xiàn)真正解耦。
  3. 支持單/組合模塊編譯,提升編譯速度,便于開發(fā)調(diào)試
    當項目隨著版本的不斷迭代,隨之增加的功能會越來越多,業(yè)務也會變得越來越復雜,最終會導致代碼量急劇上升,相關(guān)的三方sdk也會不斷的涌入,以至于,更改一處,就要全量編譯運行,有時候甚至會出現(xiàn),改一行而等10分鐘的情況,非常的耗時,大大降低了開發(fā)效率。而采取了組件化的方式后,相關(guān)業(yè)務模塊,進行單獨抽取,使得每個業(yè)務模塊可以獨立當做App存在,和其他模塊,互不關(guān)聯(lián)影響,在編譯運行時期,只考慮本模塊即可,從而減少了代碼的編譯量,提高了編譯運行速度,節(jié)約了開發(fā)時間。

組件之間數(shù)據(jù)交互怎么實現(xiàn)?

  • Broadcastreceiver
  • EventBus
  • Arouter

聊聊你對Arouter的理解

ARouter是阿里巴巴的一個組件化開源的路由框架。
ARouter通過 APT 技術(shù),生成 保存路徑(路由path)被注解(@Router)的組件類 的映射關(guān)系的類,利用這些保存了映射關(guān)系的類,Arouter根據(jù)用戶的請求 postcard(明信片) 尋找到要跳轉(zhuǎn)的 目標地址(class) ,使用 Intent跳轉(zhuǎn)。原理很簡單,框架的核心是利用APT生成的映射關(guān)系。

流程
通過APT技術(shù),尋找到所有帶有注解@Router的組件,將其注解值path和對應的Activity保存到一個map。
然后在初始化的時候?qū)⑦@個map加載到內(nèi)存中,需要的時候直接get(path)就可以了。
這樣,兩個模塊不用相互有任何直接的依賴,就可以進行轉(zhuǎn)跳,模塊與模塊之間就相互獨立了。
優(yōu)化
利用APT、JavaPoet完成了代碼生成的工作,對于一個大型項目,組件數(shù)量會很多,可能會有一兩百或者更多,把這么多組件都放到這個Map里,顯然會對內(nèi)存造成很大的壓力,因此,Arouter采用的方法就是“分組+按需加載”,分組還帶來的另一個好處是便于管理。

解決步驟一:分組
Arouter在一層map之外,增加了一層map,WareHouse這個類,里面有兩個靜態(tài)Map:

static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();
static Map<String, RouteMeta> routes = new HashMap<>();
  • groupsIndex 保存了 group 名字到 IRouteGroup 類的映射,這一層映射就是Arouter新增的一層映射關(guān)系。

  • routes 保存了路徑 pathRouteMeta 的映射,其中,RouteMeta是目標地址的包裝。這一層映射關(guān)系跟我門自己方案里的map是一致的,我們路徑跳轉(zhuǎn)也是要用這一層來映射。

IRouteGroup和RouteMeta,后者很簡單,就是對跳轉(zhuǎn)目標的封裝,我們后續(xù)稱其為“目標”,其內(nèi)包含了目標組件的信息,比如Activity的Class。那IRouteGroup是個什么東西?

public interface IRouteGroup {/*** Fill the atlas with routes in group.*/void loadInto(Map<String, RouteMeta> atlas);
}

Arouter通過apt技術(shù),為每個組生成了一個以Arouter$$Group開頭的類,這個類負責向atlas這個Hashmap中填充組內(nèi)路由數(shù)據(jù)
IRouteGroup正如其名字,它就是一個能裝載該組路由映射數(shù)據(jù)的類,其實有點像個工具類,為了方便后續(xù)講解,我們姑且稱上面這樣一個實現(xiàn)了IRouteGroup的類叫做“組加載器”

for (String className : routerMap) {if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR +SUFFIX_ROOT)) {((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);}
}
public interface IRouteRoot {/*** Load routes to input* @param routes input*/void loadInto(Map<String, Class<? extends IRouteGroup>> routes);
}

這個類實現(xiàn)了IRouteRoot,在loadInto方法中,他將組名和組對應的“組加載器”保存到了routes這個map中。也就是說,這個類將所有的“組加載器”給索引了下來,通過任意一個組名,可以找到對應的“組加載器”,我們再回到前面講的初始化Arouter時候的方法中:

((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);

WareHouse中的groupsIndex保存的是組名到“組加載器”的映射關(guān)系
Arouter的分組設計:Arouter在原來path到目標的map外,加了一個新的map,該map保存了組名到“組加載器”的映射關(guān)系。其中“組加載器”是一個類,可以加載其組內(nèi)的path到目標的映射關(guān)系。

解決步驟二:按需加載

Arouter.getInstance().build("main/hello").navigation;
最終走到
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {try {//請關(guān)注這一行LogisticsCenter.completion(postcard);} catch (NoRouteFoundException ex) {logger.warning(Consts.TAG, ex.getMessage());....//簡化代碼}//調(diào)用Intent跳轉(zhuǎn)return _navigation(context, postcard, requestCode, callback)
//從緩存里取路由信息
RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
//如果為空,需要加載該組的路由
if (null == routeMeta) {Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();iGroupInstance.loadInto(Warehouse.routes);Warehouse.groupsIndex.remove(postcard.getGroup());
} 
//如果不為空,走后續(xù)流程
else {postcard.setDestination(routeMeta.getDestination());...
}
  • 首先從Warehouse.routes(前面說了,這里存放的是path到目標的映射)里拿到目標信息,如果找不到,說明這個信息還沒加載,需要加載,實際上,剛開始這個routes里面什么都沒有。
  • 加載流程:首先從Warehouse.groupsIndex里獲取“組加載器”,組加載器是一個類,需要通過反射將其實例化,實例化為iGroupInstance,接著調(diào)用組加載器的加載方法loadInto,將該組的路由映射關(guān)系加載到Warehouse.routes中,加載完成后,routes中就緩存下來當前組的所有路由映射了,因此這個組加載器其實就沒用了,為了節(jié)省內(nèi)存,將其從Warehouse.groupsIndex移除。
  • 如果之前加載過,則在Warehouse.routes里面是可以找到路有映射關(guān)系的,因此直接將目標信息routeMeta傳遞給postcard,保存在postcard中,這樣postcard就知道了最終要去哪個組件了。

在這里插入圖片描述

APT是什么

APT(Annotation Processing Tool) 注解處理器 可以在編譯期掃描注解幫我們提前生成類
實現(xiàn)流程

  • 添加autoService和JavaPoet依賴
  • 創(chuàng)建兩個JavaLibrary,一個用來定義注解,一個用來掃描注解
  • 動態(tài)生成類和方法用IO生成文件

1. 注解Lib中創(chuàng)建一個注解類

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface Print {}

2. 掃描注解的Lib添加依賴

dependencies {//自動注冊,動態(tài)生成 META-INF/...文件implementation 'com.google.auto.service:auto-service:1.0-rc6'annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'//依賴apt-annotationimplementation project(path: ':apt-annotation')
}

3. 創(chuàng)建掃描注解的類
添加 @AutoService (Processor.class) 注解
繼承AbstractProcessor

在這里插入圖片描述
4. 解析注解
真正解析注解的地方是在process方法

/*** 掃描注解回調(diào)*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {//拿到所有添加Print注解的成員變量Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Print.class);for (Element element : elements) {//拿到成員變量名Name simpleName = element.getSimpleName();//輸出成員變量名processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,simpleName);}return false;
}

5. 生成類
用字符串拼出來一個工具類,然后用IO流寫到本地就ok了,或者使用JavaPoet提供的方法生成類
在這里插入圖片描述
總結(jié)

  • JavaPoet可以幫我們優(yōu)雅的生成類,再也不用拼接了
  • APT最主要的功能就是可以替代反射的一些功能,避免降低性能
  • APT只會在編譯時影響一點點速度,在運行期不會,而反射剛好相反

什么是注解?有哪些注解?

Java注解是一種元數(shù)據(jù),它們可以在Java源代碼中添加額外的信息,這些信息可以被編譯器工具運行時環(huán)境使用。Java注解可以用來提供類、方法、字段或其他程序元素的附加信息,以及在編譯時執(zhí)行某些任務
Java注解有三種類型:預定義注解、元注解自定義注解

  • 預定義注解
    Java中有三種內(nèi)置注解,這些注解用來為編譯器提供指令,它們是:

    • @Deprecated
      ? 這個元素是用來標記過時的元素,想必大家在日常開發(fā)中經(jīng)常碰到。編譯器在編譯階段遇到這個注解時會發(fā)出提醒警告,告訴開發(fā)者正在調(diào)用一個過時的元素比如過時的方法、過時的類、過時的成員變量
      ? 可以用來標記類,方法,屬性;

    • @Override
      ? 用來修飾對父類進行重寫的方法。如果一個并非重寫父類的方法使用這個注解,編譯器將提示錯誤。
      ? 實際上在子類中重寫父類或接口的方法,@Overide并不是必須的。但是還是建議使用這個注解,在某些情況下,假設你修改了父類的方法的名字,那么之前重寫的子類方法將不再屬于重寫,如果沒有@Overide,你將不會察覺到這個子類的方法。有了這個注解修飾,編譯器則會提示你這些信息

    • @SuppressWarnings
      ? 用來抑制編譯器生成警告信息
      ? 可以修飾的元素為類,方法,方法參數(shù),屬性,局部變量
      ? 當我們一個方法調(diào)用了棄用的方法或者進行不安全的類型轉(zhuǎn)換,編譯器會生成警告。我們可以為這個方法增加@SuppressWarnings注解,來抑制編譯器生成警告。
      ? 注意:使用@SuppressWarnings注解,采用就近原則,比如一個方法出現(xiàn)警告,我們盡量使用@SuppressWarnings注解這個方法,而不是注解方法所在的類。雖然兩個都能抑制編譯器生成警告,但是范圍越小越好,因為范圍大了,不利于我們發(fā)現(xiàn)該類下其他方法的警告信息

  • 元注解
    java.lang.annotation提供了四種元注解,專門注解其他的注解(在自定義注解的時候,需要使用到元注解).

    • 1.@Documented - 注解是否將包含在JavaDoc中
      ? 一個簡單的Annotations標記注解,表示是否將注解信息添加在javadoc文檔中

    • 2.@Retention –什么時候使用該注解
      Retention 的英文意為保留期的意思。當 @Retention 應用到一個注解上的時候,它解釋說明了這個注解的的存活時間

      • RetentionPolicy.SOURCE 注解只在源碼階段保留,在編譯器進行編譯時它將被丟棄忽視。
      • RetentionPolicy.CLASS 注解只被保留到編譯進行的時候,它并不會被加載到 JVM 中。
      • RetentionPolicy.RUNTIME 注解可以保留到程序運行的時候,它會被加載進入到 JVM 中,所以在程序運行時可以獲取到它們。
    • 3.@Target– 注解用于什么地方
      ? 默認值為任何元素,表示該注解用于什么地方??捎玫腅lementType參數(shù)包括

      • ElementType.CONSTRUCTOR:用于描述構(gòu)造器
      • ElementType.FIELD:成員變量、對象、屬性(包括enum實例)
      • ElementType.LOCAL_VARIABLE:用于描述局部變量
      • ElementType.METHOD:用于描述方法
      • ElementType.PACKAGE:用于描述包
      • ElementType.PARAMETER:用于描述參數(shù)
      • ElementType.TYPE:用于描述類、接口(包括注解類型) 或enum聲明
    • 4.@Inherited – 定義該注釋和子類的關(guān)系

? Inherited 是繼承的意思,但是它并不是說注解本身可以繼承,而是說如果一個超類被 @Inherited 注解過的注解進行注解的話,那么如果它的子類沒有被任何注解應用的話,那么這個子類就繼承了超類的注解

  • 自定義注解

      	自定義注解類編寫的一些規(guī)則:1. Annotation型定義為@interface, 所有的Annotation會自動繼承java.lang.Annotation這一接口,并且不能再去繼承別的類或是接口.2. 參數(shù)成員只能用public或默認(default)這兩個訪問權(quán)修飾3. 參數(shù)成員只能用基本類型byte,short,char,int,long,float,double,boolean八種基本數(shù)據(jù)類型和String、Enum、Class、annotations等數(shù)據(jù)類型,以及這一些類型的數(shù)組.4. 要獲取類方法和字段的注解信息,必須通過Java的反射技術(shù)來獲取 Annotation對象,因為你除此之外沒有別的獲取注解對象的方法5. 注解也可以沒有定義成員, 不過這樣注解就沒啥用了
    
@Target(ElementType.FIELD)//可以用來修飾對象,屬性
@Retention(RetentionPolicy.RUNTIME)//什么時候都不丟棄
@Documented//用于生成javadoc文檔
public @interface FruitName {String value() default "";
}
@Target(ElementType.FIELD)//可以用來修飾對象,屬性
@Retention(RetentionPolicy.RUNTIME)//什么時候都不丟棄
@Documented//用于生成javadoc文檔
public @interface FruitColor {public enum Color{RED,GREEN,BLUE};Color fc() default Color.GREEN;
}

創(chuàng)建一個工具類用來處理注解

package cn.sz.gl.test07;
import java.lang.reflect.Field;
public class AnnotationUtil {public static Object findInfo(Class clazz) throws IllegalArgumentException, IllegalAccessException, InstantiationException{Object obj = clazz.newInstance();Field fs [] = clazz.getDeclaredFields();for (int i = 0; i < fs.length; i++) {//判斷該屬性上是否有FruitName類型的注解if(fs[i].isAnnotationPresent(FruitName.class)){FruitName fn = fs[i].getAnnotation(FruitName.class);//為屬性賦值fs[i].setAccessible(true);fs[i].set(obj, fn.value());}if(fs[i].isAnnotationPresent(FruitColor.class)){FruitColor fc = fs[i].getAnnotation(FruitColor.class);fs[i].setAccessible(true);fs[i].set(obj, fc.fc().toString());}}return obj;}
}

2. 插件化

所謂插件化,是實現(xiàn)動態(tài)化的一種具體的技術(shù)手段。

  • 布局動態(tài)化。通過下發(fā)配置,再由客戶端映射為具體的原生布局,實現(xiàn)動態(tài)化。這種方案的性能還不錯,但只適合布局動態(tài)化,更新業(yè)務邏輯則較為困難。
  • H5容器。其實webview就是一個天然可實現(xiàn)動態(tài)化的方案。這種方案的穩(wěn)定性和動態(tài)化能力都不錯,主要缺陷是性能較差,畢竟js是解釋型語言,終究比不過原生。
  • 虛擬運行環(huán)境。如Flutter。Flutter所使用的Dart語言既是解釋型語言,又是編譯型語言,解決了上面提到的性能問題。但這種方案往往需要在apk中依賴一個sdk,增加了包大小。
  • 插件化。插件化通過動態(tài)下發(fā)部分代碼,來實現(xiàn)動態(tài)的功能更新。前幾年,插件化是較火的方案,近些年受制于系統(tǒng)的限制,變得越來越難以實現(xiàn)。

Shadow
通過運用AOP思想,利用字節(jié)碼編輯工具,在編譯期把插件中的所有Activity的父類都改成一個普通類,然后讓殼子持有這個普通類型的父類去轉(zhuǎn)調(diào)它就不用Hack任何系統(tǒng)實現(xiàn)了。雖然說是非常簡單的事,實際上這樣修改后還帶來一些額外的問題需要解決,比如getActivity()方法返回的也不是Activity了。不過Shadow的實現(xiàn)中都解決了這些問題。
在這里插入圖片描述

Flutter

1. dart中的作用域與了解嗎

默認是public,如需私有只需要在變量名或者方法名前加_

例如 
var user = "小王"; //是public
var _user= "小王"; //是private

2. dart中. … …分別是什么意思?

一個點 .
是正常的對象訪問
兩個點 . .
意思是 「級聯(lián)操作符」,為了方便配置而使用?!浮购汀?」不同的是 調(diào)用「…」后返回的相當于是 this, 可以實現(xiàn)對一個對象的連續(xù)調(diào)用

          Paint()..color = thumbColor..style = PaintingStyle.stroke..strokeCap = StrokeCap.round..strokeWidth = tempWidth);

三個點 用來拼接集合,如List,Map等

class TestDemo { TestDemo() {    var list2 = ['d', 'e', 'f'];var list = ['a', 'b', 'c', ...list2];// 打印結(jié)果:// 這里組合后 list就變成[ 'a', 'b', 'c','d', 'e', 'f']var map2 = {'a': 'a', 'b': 'b'};var map = {...map2, 'c': 'c', 'd': 'd'};// 打印結(jié)果:// 這里組合后map就變成{'a': 'a', 'b': 'b','c': 'c', 'd': 'd'}}
}

3. Dart 是不是單線程模型?如何運行的?

Dart是單線程模型
Dart在單線程中是以消息循環(huán)機制來運行的,其中包含兩個任務隊列,一個是 “微任務隊列” microtask queue,另一個叫做 “事件隊列” event queue微任務隊列的執(zhí)行優(yōu)先級高于 事件隊列。

Dart大致運行原理:先開啟app執(zhí)行入口函數(shù)main(),執(zhí)行完成之后,消息機制啟動,先是會按照先進先出的順序逐個執(zhí)行微任務隊列中的任務microtask事件任務eventtask 執(zhí)行完畢后便會退出,但是,在事件任務執(zhí)行的過程中也可以插入新的微任務和事件任務,在這種情況下,整個線程的執(zhí)行過程便是一直在循環(huán),不會退出,而Flutter中,主線程的執(zhí)行過程正是如此,永不終止。
在這里插入圖片描述
事件循環(huán)中,當某個任務發(fā)生異常并沒有被捕獲時,程序并不會退出,而直接導致的結(jié)果是當前任務的后續(xù)代碼就不會被執(zhí)行了,也就是說一個任務中的異常是不會影響其它任務執(zhí)行的

Dart 中事件的執(zhí)行順序:Main > MicroTask > EventQueue

  • 通常使用 scheduleMicrotask(…) 或者 Future.microtask(…) 方法向微任務隊列插入一個任務。
  • 通常使用 FutureEventQueue加入事件,也可以使用 async 和 await 向 EventQueue 加入事件。

4. Dart既然是單線程模型支持多線程嗎?

多線程語言如Java實現(xiàn)異步的方式是將耗時操作新開子線程去執(zhí)行。Dart是單線程模型,沒有多線程的概念,他是通過Future和Stream來實現(xiàn)異步的。

5. Future是什么

Future是異步函數(shù),它可以在不阻塞當前任務的情況下執(zhí)行一個任務,并在任務完成后獲得相應的結(jié)果。
Future實際上是將其中的事件放入到了Event Queue事件隊列中執(zhí)行。
常與async一起使用
Async是Dart中的一個關(guān)鍵字,用于標記異步函數(shù)。async函數(shù)返回一個Future對象,并且可以使用await關(guān)鍵字來等待函數(shù)的執(zhí)行結(jié)果。例如:

Future<String> getData(String url) async {
var response = await http.get(url);
return response.body;
}

6. Stream是什么

Stream 流是一個異步的事件隊列。分為單訂閱流廣播訂閱

  • 單訂閱流
    默認情況下創(chuàng)建的流都是單訂閱流,單訂閱流只能被訂閱一次,第二次監(jiān)聽會報錯!監(jiān)聽開始之前的元素不會被訂閱。但 Stream 可以通過 transform() 方法(返回另一個 Stream)進行連續(xù)調(diào)用。通過 Stream.asBroadcastStream() 可以將一個單訂閱模式的 Stream 轉(zhuǎn)換成一個多訂閱模式的 Stream.isBroadcast 屬性可以判斷當前 Stream 所處的模式。
  • 廣播訂閱
    廣播流允許存在任意數(shù)量的 listener,并且無論是否存在 listener,它都能產(chǎn)生事件,所以中途加入的 listener 不會偵聽到已發(fā)生的事件。

單訂閱流常用于數(shù)據(jù)的傳遞,廣播流用于事件的分發(fā)。

StreamController 是流控制器的核心接口,包含了流控制器該有的大多數(shù)接口方法。其中

  • stream 用于向外提供創(chuàng)建的Stream。
  • sink 是該控制器關(guān)聯(lián)的流的數(shù)據(jù)來源??梢允褂胹ink.add 方法向流中添加數(shù)據(jù)。
  • onListen, 當控制器中的流被監(jiān)聽的時候,會回調(diào)該方法。
  • onPause, 當流的監(jiān)聽主動暫停的時候,會回調(diào)該方法。
  • onResume, 當流的監(jiān)聽主動恢復監(jiān)聽的時候,會回調(diào)該方法。
  • onCancel,當流的監(jiān)聽取消監(jiān)聽的時候,會回調(diào)該方法。

7. Flutter 如何和原生交互

Flutter 與原生交互使用Platform Channel。Flutter定義了三種不同類型的Channel

  • BasicMessageChannel 用于傳遞字符串和半結(jié)構(gòu)化的信息,可持續(xù)通信,收到消息后可以回復此次消息。場景:消息互發(fā)(雙向有返回值,可持續(xù)通信)
  • MethodChannel 用于傳遞方法調(diào)用。場景:native與flutter的方法調(diào)用(雙向有返回值,一次性通信)
  • EventChannel 用于事件型的通信,僅支持 native 到 Flutter 的單向傳遞。場景:通常用于狀態(tài)端監(jiān)聽,比如網(wǎng)絡變化、傳感器數(shù)據(jù)、電量更新或聲音改變(僅支持數(shù)據(jù)單向傳遞,無返回值)

8. 說一下 mixin?

  • mixin 可以理解為對類的一種“增強”,但它與單繼承兼容,因為它的繼承關(guān)系是線性的。
  • with 后面的類會覆蓋前面的類的同名方法
  • 當我們想要在不共享相同類層次結(jié)構(gòu)的多個類之間共享行為時,可以使用 mixin
  • on限定了使用mixin組塊的宿主必須要繼承于某個特定的類;在mixin中可以訪問到該特定類的成員和方法。
  • 作為mixin的類不能有自定義構(gòu)造方法

9. StatefulWidget 的生命周期

在這里插入圖片描述

  • initState 初始化階段回調(diào)
  • didUpdateWidget build階段回調(diào) 當Widget配置發(fā)生變化時,比如父Widget觸發(fā)重建(即父Widget的狀態(tài)發(fā)生變化時),熱重載,系統(tǒng)會調(diào)用這個函數(shù)。
  • didChangeDependencies build階段回調(diào) state對象依賴關(guān)系發(fā)生變化后,flutter會進行回調(diào)。
  • deactivate 不可見時回調(diào)
  • dispose 銷毀時回調(diào)

10. main()和runApp()函數(shù)在flutter的作用分別是什么?有什么關(guān)系嗎?

main函數(shù)是程序執(zhí)行的入口。
runApp是Flutter應用啟動的入口。runApp中啟動了Flutter FrameWork并且完成根widget樹的渲染
在runApp中 初始化了WidgetsFlutterBinding這個類混入了七個BindingBase子類。同時調(diào)用WidgetsFlutterBindingFlutter Framework綁定到Flutter Engine上面

  • GestureBinding:綁定手勢系統(tǒng)。
  • ServicesBinding:主要作用與defaultBinaryMessenger有關(guān),用于和native通訊相關(guān)。
  • SchedulerBinding:該類主要用于調(diào)度幀渲染相關(guān)事件。
  • PaintingBinding:和painting庫綁定,處理圖片緩存。
  • SemanticsBinding:將語義層和Flutter Engine綁定起來。
  • RendererBinding:將渲染樹與Flutter Engine綁定起來。
  • WidgetsBinding:將Widget層與Flutter Engine綁定起來。

11. 怎么理解Isolate?

isolate 意思是隔離。它可以理解為 Dart 中的線程。isolate與線程的區(qū)別就是線程與線程之間是共享內(nèi)存的,而 isolate 和 isolate 之間是不共享的。因此也不存在鎖競爭問題,兩個Isolate完全是兩條獨立的執(zhí)行線,且每個Isolate都有自己的事件循環(huán),它們之間只能通過發(fā)送消息通信,它的資源開銷低于線程。
每個 isolate 都擁有自己的事件循環(huán)隊列(MicroTask 和 Event)。這意味著在一個 isolate 中運行的代碼與另外一個 isolate 不存在任何關(guān)聯(lián)。
isolate之間的通信
由于isolate之間沒有共享內(nèi)存,他們之間的通信唯一方式只能是通過Port進行,而且Dart中的消息傳遞總是異步的。

  • 兩個Isolate是通過兩對Port對象通信,一對Port分別由用于接收消息的ReceivePort對象,和用于發(fā)送消息的SendPort對象構(gòu)成。
  • Flutter 中可以使用compute函數(shù)來創(chuàng)建Isolate
    • compute中運行的函數(shù),必須是頂級函數(shù)或者是static函數(shù),
    • compute傳參,只能傳遞一個參數(shù),返回值也只有一個

12. 簡單介紹下Flutter框架,以及它的優(yōu)缺點?

Flutter 是Google開發(fā)的一款跨平臺方案的開源框架。
優(yōu)點

  • 高性能:自帶繪制系統(tǒng),讓Flutter擁有原生級別的性能。
    RN、Weex等跨平臺開發(fā)框架,都選擇將繪制任務交給了平臺原生的繪制引擎。因此,這些框架在規(guī)避了重新開發(fā)一套圖形機制的同時,也從底層機制上帶來了無法調(diào)和的性能問題。
  • 因為Flutter底層使用和Android原生一樣的Skia引擎,所以保證了android和ios兩端的UI一致性
  • 擁有熱更新機制,開發(fā)效率比原生更高
  • Dart語言比較簡單,而且Flutter語法與JectPack庫中的Compose接近,容易上手。

缺點

  • 生態(tài)還不夠豐富
  • 缺少動態(tài)性

12. 簡述Widgets、RenderObjects 和 Elements的關(guān)系

  • Widget :僅用于存儲渲染所需要的信息。Widget只是配置信息,相當于Android里的XML布局文件
  • RenderObject :負責管理布局、繪制等操作。保存了元素的大小,布局等信息.
  • Element :是這顆巨大的控件樹上的實體。通過 WidgetcreateElement() 方法,根據(jù) Widget數(shù)據(jù)生成。如果 widget 有相同的 runtimeType 并且有相同的 key, Element 可以根據(jù)新的 widget 進行 update.

Widget會被inflate(填充)到Element,并由Element管理底層渲染樹。Widget并不會直接管理狀態(tài)及渲染,而是通過State這個對象來管理狀態(tài)。Flutter創(chuàng)建Element的可見樹,相對于Widget來說,是可變的,通常界面開發(fā)中,我們不用直接操作Element,而是由框架層實現(xiàn)內(nèi)部邏輯。就如一個UI視圖樹中,可能包含有多個TextWidget(Widget被使用多次),但是放在內(nèi)部視圖樹的視角,這些TextWidget都是填充到一個個獨立的Element中。Element會持有renderObject和widget的實例。記住,Widget 只是一個配置RenderObject 負責管理布局、繪制等操作。

在第一次創(chuàng)建 Widget 的時候,會對應創(chuàng)建一個 Element, 然后將該元素插入樹中。如果之后 Widget 發(fā)生了變化,則將其與舊的 Widget 進行比較,并且相應地更新 Element。重要的是,Element 不會被重建,只是更新而已。

  • 一個Widget一定對應一個Element,但是不一定會有RenderObject
  • 只有當Widget被渲染顯示出來時才會有RenderObject

13. 介紹下Widget、State、Context 概念

  • Widget:在Flutter中,幾乎所有東西都是Widget。將一個Widget想象為一個可視化的組件(或與應用可視化方面交互的組件),當你需要構(gòu)建與布局直接或間接相關(guān)的任何內(nèi)容時,你正在使用Widget。
  • Widget樹:Widget以樹結(jié)構(gòu)進行組織。包含其他Widget的widget被稱為父Widget(或widget容器)。包含在父widget中的widget被稱為子Widget。
  • Context僅僅是已創(chuàng)建的所有Widget樹結(jié)構(gòu)中的某個Widget的位置引用。簡而言之,將context作為widget樹的一部分,其中context所對應的widget被添加到此樹中。一個context只從屬于一個widget,它和widget一樣是鏈接在一起的,并且會形成一個context樹。
  • State定義了StatefulWidget實例的行為,它包含了用于”交互/干預“Widget信息的行為和布局。應用于State的任何更改都會強制重建Widget。

14. 簡述Widget的StatelessWidget和StatefulWidget兩種狀態(tài)組件類

  • StatelessWidget: 一旦創(chuàng)建就不關(guān)心任何變化,在下次構(gòu)建之前都不會改變。它們除了依賴于自身的配置信息(在父節(jié)點構(gòu)建時提供)外不再依賴于任何其他信息。比如典型的Text、Row、Column、Container等,都是StatelessWidget。它的生命周期相當簡單:初始化、通過build()渲染。

  • StatefulWidget: 在生命周期內(nèi),該類Widget所持有的數(shù)據(jù)可能會發(fā)生變化,這樣的數(shù)據(jù)被稱為State,這些擁有動態(tài)內(nèi)部數(shù)據(jù)的Widget被稱為StatefulWidget。比如復選框、Button等。State會與Context相關(guān)聯(lián)并且此關(guān)聯(lián)是永久性的,State對象將永遠不會改變其Context,即使可以在樹結(jié)構(gòu)周圍移動,也仍將與該context相關(guān)聯(lián)。當state與context關(guān)聯(lián)時,state被視為已掛載。StatefulWidget由兩部分組成,在初始化時必須要在createState()時初始化一個與之相關(guān)的State對象。

15. 什么是狀態(tài)管理,你了解哪些狀態(tài)管理框架?

Flutter中的狀態(tài)和前端React中的狀態(tài)概念是一致的。React框架的核心思想是組件化,應用由組件搭建而成,組件最重要的概念就是狀態(tài),狀態(tài)是一個組件的UI數(shù)據(jù)模型,是組件渲染時的數(shù)據(jù)依據(jù)。

Flutter的狀態(tài)可以分為全局狀態(tài)和局部狀態(tài)兩種。常用的狀態(tài)管理有GetX和Provider

16. 簡述Flutter的繪制流程

Flutter只關(guān)心向 GPU提供視圖數(shù)據(jù),GPU的 VSync信號同步到 UI線程,UI線程使用 Dart來構(gòu)建抽象的視圖結(jié)構(gòu),這份數(shù)據(jù)結(jié)構(gòu)在 GPU線程進行圖層合成,視圖數(shù)據(jù)提供給 Skia引擎渲染為 GPU數(shù)據(jù),這些數(shù)據(jù)通過 OpenGL或者 Vulkan提供給 GPU。
在這里插入圖片描述

17. await for 如何使用?

await for是不斷獲取stream流中的數(shù)據(jù),然后執(zhí)行循環(huán)體中的操作。它一般用在直到stream什么時候完成,并且必須等待傳遞完成之后才能使用,不然就會一直阻塞。

Stream<String> stream = new Stream<String>.fromIterable(['開心', '面試', '過', '了']);
main() async{await for(String s in stream){print(s);}

18. 介紹下Flutter的架構(gòu)

Flutter框架自下而上分為Embedder、EngineFramework三層。

  • Embedder是操作系統(tǒng)適配層,實現(xiàn)了渲染 Surface設置,線程設置,以及平臺插件等平臺相關(guān)特性的適配;
  • Engine層負責圖形繪制、文字排版和提供Dart運行時,Engine層具有獨立虛擬機,正是由于它的存在,Flutter程序才能運行在不同的平臺上,實現(xiàn)跨平臺運行;
  • Framework層則是使用Dart編寫的一套基礎(chǔ)視圖庫,包含了動畫、圖形繪制和手勢識別等功能,是使用頻率最高的一層。
    在這里插入圖片描述

19. 介紹下Flutter的FrameWork層和Engine層,以及它們的作用

  • Flutter的FrameWork層是用Drat編寫的框架(SDK),它實現(xiàn)了一套基礎(chǔ)庫,包含Material(Android風格UI)和Cupertino(iOS風格)的UI界面,下面是通用的Widgets(組件),之后是一些動畫、繪制、渲染、手勢庫等。這個純 Dart實現(xiàn)的 SDK被封裝為了一個叫作 dart:ui的 Dart庫。我們在使用 Flutter寫 App的時候,直接導入這個庫即可使用組件等功能。
  • Flutter的Engine層是Skia 2D的繪圖引擎庫,其前身是一個向量繪圖軟件,Chrome和 Android均采用 Skia作為繪圖引擎。Skia提供了非常友好的 API,并且在圖形轉(zhuǎn)換、文字渲染、位圖渲染方面都提供了友好、高效的表現(xiàn)。Skia是跨平臺的,所以可以被嵌入到 Flutter的 iOS SDK中,而不用去研究 iOS閉源的 Core Graphics / Core Animation。Android自帶了 Skia,所以 Flutter Android SDK要比 iOS SDK小很多。

20. Dart中var與dynamic的區(qū)別

var 和 dynamic 可以用來聲明一個可以接受任何類型的變量

  • var 是編譯時確定類型一旦賦值,類型推斷就會確定這個變量的類型 ,由于Dart是個強類型語言,所以不能在以后的賦值中變更其類型。
  • dynamic 是運行時確定類型,且在后續(xù)賦值中可以改變類型。

21. const關(guān)鍵字與final關(guān)鍵字的區(qū)別

  • final 用來修飾變量,運行時賦值、只能被賦值一次。
  • const 只可以修飾變量,常量構(gòu)造函數(shù),編譯時確定。
  • final 修飾的對象內(nèi)容一樣會指向不同的地址,const 修飾的對象內(nèi)容一樣會指向相同的地址

22. Flutter在Debug和Release下分別使用什么編譯模式,有什么區(qū)別?

Flutter在Debug下使用JIT模式即Just in time(即時編譯),在Release下使用AOT模式即Ahead of time(提前編譯)

  • JIT模式因為需要邊運行邊編譯,所以會占用運行時內(nèi)存,導致卡頓現(xiàn)象,但是有動態(tài)編譯效果對于開發(fā)者來說非常方便調(diào)試。
  • AOT模式提前編譯不會占用運行時內(nèi)存,相對來說運行流暢,但是會導致編譯時間增加。

23. 什么是Key?

Flutter key子類包含 LocalKeyGlobalKey 。在Flutter中,Key是不能重復使用的,所以Key一般用來做唯一標識。組件在更新的時候,其狀態(tài)的保存主要是通過判斷組件的類型或者key值是否一致。

  • GlobalKey 能夠跨 Widget 訪問狀態(tài)。在整個APP范圍內(nèi)唯一
  • LocalKey,在兄弟節(jié)點中唯一
    Widget相當于配置文件,是不可變的,想改變只能重建,而Element是Widget實體,創(chuàng)建后只會更新不會重建。

Widget可以通過Key來更新Element

24. future 和steam有什么不一樣?

在 Flutter 中有兩種處理異步操作的方式 FutureStream,Future 用于處理單個異步操作,Stream 用來處理連續(xù)的異步操作。

25. 什么是widget? 在flutter里有幾種類型的widget?

widget在flutter里基本是一些UI組件
有兩種類型的widget,分別是statefulWidgetstatelessWidget兩種

  • statelessWidget不會自己重新構(gòu)建自己
  • statefulWidget會重新構(gòu)建自己

26. statefulWidget更新流程了解嗎

  1. 觸發(fā)setState方法:當調(diào)用setState方法時會在State類中調(diào)用setState方法
@protected
void setstate(VoidCallback fn){
final dynamic result fn()as dynamic;
_element.markNeedsBuild();
}
  1. element標臟StatefulElement中執(zhí)行的markNeedsBuild方法。在markNeedsBuild函數(shù)中將當前樹進行標臟(標記為dirty),如果己經(jīng)是臟樹,那么直接返回。owner:是BuildOwner,BuildOwner是element的管理類,主要負責dirtyElement、ina
    ctiveElement、globalkey關(guān)聯(lián)的element的管理。
void markNeedsBuild(){
if (!_active)
return;
if (dirty)
return;
_dirty true;
owner.scheduleBuildFor(this);
}
  1. 將element添加到臟列表_dirtyElements中,并觸發(fā)WidgetsBindingl的回調(diào)
    scheduleBuildFor方法在BuildOwner中執(zhí)行。onBuildScheduled()回調(diào)在WidgetsBinding類中執(zhí)行。BuildOwner對象執(zhí)行onBuildScheduled() 回調(diào)時,會去執(zhí)行WidgetsBinding類中的handleBuildScheduled() 方法。
final List_dirtyElements [];
......
void scheduleBuildFor(Element element){
......
if (element._inDirtyList){
_dirtyElementsNeedsResorting true;
return;
}
if (!_scheduledFlushDirtyElements && onBuildScheduled !=null){
_scheduledFlushDirtyElements true;
//是一個回調(diào)方法
onBuildScheduled();
}
dirtyElements.add(element);
element._inDirtyList true;
}
  1. WidgetsBinding中觸發(fā)onBuildScheduled的回調(diào),執(zhí)行ensurevisualUpdate
    方法。
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {@overridevoid initInstances() {super.initInstances();_instance = this;assert(() {_debugAddStackFilters();return true;}());_buildOwner = BuildOwner();buildOwner!.onBuildScheduled = _handleBuildScheduled;platformDispatcher.onLocaleChanged = handleLocaleChanged;SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);assert(() {FlutterErrorDetails.propertiesTransformers.add(debugTransformDebugCreator);return true;}());platformMenuDelegate = DefaultPlatformMenuDelegate();}
.....void _handleBuildScheduled() {
....ensureVisualUpdate();}
  1. SchedulerBinding中觸發(fā)ensureVisualUpdate的方法,根據(jù)不同的調(diào)度任務執(zhí)
    行不同的操作。
void ensurevisualUpdate()
switch (schedulerPhase){
//沒有正在處理的幀,可能正在執(zhí)行的是WidgetsBinding.scheduleTask,
//scheduleMicrotask,Timer,事件handlers,或者其他回調(diào)等
case SchedulerPhase.idle:
//主要是清理和計劃執(zhí)行下一幀的工作
case SchedulerPhase.postFrameCallbacks:
scheduleFrame();//清求新的貞渲染。
return;
//SchedulerBinding.handleBeginFrame過程,
處理動畫狀態(tài)更新
case SchedulerPhase.transientCallbacks:
//處理transientCal1 backs階段觸發(fā)的微任務(Microtasks)
case SchedulerPhase.midFrameMicrotasks:
//WidgetsBinding.drawFrame和SchedulerBinding.handleDrawFrame過程,
//bui1d/1 ayout/paint流水線江作
case SchedulerPhase.persistentCallbacks:
return;
}
  1. SchedulerBinding請求新的frame,注冊Vsync信號。Flutter在window上注冊一個onBeginFrame和一個onDrawFrame回調(diào),在onDrawFrame回調(diào)中最終會調(diào)用drawFrame。
    當我們調(diào)用window.scheduleFrame() 方法之后,Flutter引擎會在合適的時機(可
    以認為是在屏幕下一次刷新之前,具體取決于Flutter引擎的實現(xiàn))來調(diào)用onBeginFrame和onDrawFrame。
void scheduleFrame()
if (_hasScheduledFrame!framesEnabled)
return;
//確保渲染的回調(diào)已經(jīng)被注冊
ensureFrameCallbacksRegistered();
//windowi調(diào)度幀
window.scheduleFrame();
_hasScheduledFrame true;
}
@protected
void ensureFrameCallbacksRegistered(){
/處理渲染前的任務
window.onBeginFrame ??=_handleBeginFrame;
//核心渲染流程
window.onDrawFrame ??_handleDrawFrame;}
  1. 當下一次刷新之前,會調(diào)用onBeginFrameonDrawFrame方法。
    handleBeginFrame ()
  2. 調(diào)用RendererBinding和drawFrame方法。
http://www.risenshineclean.com/news/7081.html

相關(guān)文章:

  • 高端平面設計網(wǎng)站seo優(yōu)化方式
  • 云南省城鄉(xiāng)住房與建設廳網(wǎng)站網(wǎng)頁搜索優(yōu)化
  • 洛陽網(wǎng)站seo免費推廣
  • 電子商務網(wǎng)站建設規(guī)劃書的內(nèi)容seo是搜索引擎營銷嗎
  • 鹽城網(wǎng)站建設效果google中文搜索引擎
  • 南昌縣住房和城鄉(xiāng)建設局網(wǎng)站seo文章是什么意思
  • 做外貿(mào)什么網(wǎng)站比較好游戲推廣平臺有哪些
  • 一個空間兩個php網(wǎng)站網(wǎng)絡優(yōu)化培訓騙局
  • 寧波網(wǎng)站建設服務報價百度自動優(yōu)化
  • 怎么下載建設銀行網(wǎng)站搜索引擎優(yōu)化案例
  • 做推廣用那個網(wǎng)站信息流優(yōu)化師培訓機構(gòu)
  • 又拍云wordpress優(yōu)化網(wǎng)站seo策略
  • wordpress js load谷歌seo排名工具
  • 自己制作網(wǎng)站app一手app推廣接單平臺
  • 湛江有哪些網(wǎng)站建設公司滄州百度推廣公司
  • 做網(wǎng)站圖片路徑做緩存嗎快速網(wǎng)站輕松排名
  • 網(wǎng)站優(yōu)化比較好用的軟件win10優(yōu)化大師是官方的嗎
  • 東方財富網(wǎng)官方網(wǎng)站首頁免費網(wǎng)站建站頁面
  • 南充做網(wǎng)站公司百度熱線客服24小時
  • 福州網(wǎng)站設計大概多少錢大數(shù)據(jù)查詢
  • 視頻網(wǎng)站直播怎么做的國內(nèi)搜索引擎排行榜
  • 蘇州保潔公司多少錢一個平方百度seo自然優(yōu)化
  • 公眾號里的電影網(wǎng)站怎么做的浙江百度代理公司
  • 泉州最專業(yè)手機網(wǎng)站建設定制網(wǎng)站網(wǎng)絡推廣服務
  • wordpress圖片比例拉伸廣州百度推廣優(yōu)化排名
  • 成都html5網(wǎng)站建設市場運營和市場營銷的區(qū)別
  • 網(wǎng)站設計與網(wǎng)頁制作教程網(wǎng)站搜索引擎優(yōu)化技術(shù)
  • 購物網(wǎng)站建設包括哪些做電商需要學哪些基礎(chǔ)
  • 做的好的農(nóng)產(chǎn)品網(wǎng)站肇慶疫情最新情況
  • 公司做網(wǎng)站留言板seo綜合查詢什么意思