2023-Android-常見面試題

[TOC]

HASH算法

  • 常用的摘要算法包括MD5,SHA1,SHA256

  • 消息摘要算法的特點:

    1. 無論輸入的消息有多長,計算出來的消息摘要的長度總是固定的。

    2. 消息摘要看起來是“隨機的”。這些比特看上去是胡亂的雜湊在一起的。

    3. 一般地,只要輸入的消息不同,對其進行摘要以后產生的摘要消息也必不相同;但相同的輸入必會產生相同的輸出。

    4. 消息摘要函數是無陷門的單向函數,即只能進行正向的信息摘要,而無法從摘要中恢復出任何的消息,甚至根本就找不到任何與原信息相關的信息。

    5. 好的摘要算法,無法找到兩條消息,是它們的摘要相同。

    6. 主要特征是加密過程不需要密鑰,并且經過加密的數據無法被解密,只有輸入相同的明文數據經過相同的消息摘要算法才能得到相同的密文。消息摘要算法不存在 密鑰的管理與分發問題,適合于分布式網絡相同上使用。由于其加密計算的工作量相當可觀,所以以前的這種算法通常只用于數據量有限的情況下的加密,例如計算機的口令就是 用不可逆加密算法加密的。

https握手過程

  • 非對稱加密算法用于在握手過程中加密生成的密碼

  • 對稱加密算法用于對真正傳輸的數據進行加密

  • 而HASH算法用于驗證數據的完整性。

  • RSA公私鑰密鑰加密算法,DES,AES對稱加密算法,SHA1摘要算法

  • Client端和Server端:

    • Client發出https的請求->Server

    • Server回應公鑰SPublic->Client

    • Client生成隨機數作為以后消息的加密密鑰key

    • Client用SPublic加密key發出https請求->Server

    • Server用私鑰SPrivate解密key

    • [圖片上傳失敗...(image-dd4729-1682881853345)]

第一次HTTP請求

  1. 客戶端向服務器發起HTTPS請求,連接到服務器的443端口。

  2. 服務器端有一個密鑰對,即公鑰和私鑰,是用來進行非對稱加密使用的,服務器端保存著私鑰,不能將其泄露,公鑰可以發送給任何人。

  3. 服務器將自己的公鑰發送給客戶端。

  4. 客戶端收到服務器端的公鑰之后,會對公鑰進行檢查,驗證其合法性,如果公鑰合格,那么客戶端會生成一個隨機值,這個隨機值就是用于進行對稱加密的密鑰,我們將該密鑰稱之為client key,即客戶端密鑰,這樣在概念上和服務器端的密鑰容易進行區分。然后用服務器的公鑰對客戶端密鑰進行非對稱加密,這樣客戶端密鑰就變成密文了,至此,HTTPS中的第一次HTTP請求結束。

第二次HTTP請求

  1. 客戶端會發起HTTPS中的第二個HTTP請求,將加密之后的客戶端密鑰發送給服務器。

  2. 服務器接收到客戶端發來的密文之后,會用自己的私鑰對其進行非對稱解密,解密之后的明文就是客戶端密鑰,然后用客戶端密鑰對數據進行對稱加密,這樣數據就變成了密文。

  3. 然后服務器將加密后的密文發送給客戶端。

  4. 客戶端收到服務器發送來的密文,用客戶端密鑰對其進行對稱解密,得到服務器發送的數據。這樣HTTPS中的第二個HTTP請求結束,整個HTTPS傳輸完成。

hashmap原理

  • HashMap是基于哈希表實現的,用Entry[]來存儲數據,而Entry中封裝了key、value、hash以及Entry類型的next

  • HashMap存儲數據是無序的

  • hash沖突是通過拉鏈法解決的,也就是數組+鏈表

  • HashMap的容量永遠為2的冪次方,有利于哈希表的散列

  • HashMap不支持存儲多個相同的key,且只保存一個key為null的值,多個會覆蓋

  • put過程,是先通過key算出hash,然后用hash算出應該存儲在table中的index,然后遍歷table[index],看是否有相同的key存在,存在,則更新value;不存在則插入到table[index]單向鏈表的表頭,時間復雜度為O(n)

  • get過程,通過key算出hash,然后用hash算出應該存儲在table中的index,然后遍歷table[index],然后比對key,找到相同的key,則取出其value,時間復雜度為O(n)

  • HashMap是線程不安全的,如果有線程安全需求,推薦使用ConcurrentHashMap。

socket

  • 粘包問題:包頭有包長度

  • 心跳包:5s一次登錄檢查,10s一次心跳

  • TCP的keepalive選項

Netty

  • 方便處理粘包問題,自帶由定長拆包器,分隔符拆包器等

  • 零拷貝,java的FileChannel.transferTo方法,通過bytebuffer直接進行socket的讀寫,利用ByteBuf機制進行多bytebuffer數組的合成(實際上ByteBuf只是記錄了bytebuffer的引用)

線程池

一個線程只能有一個Looper,但可以有多個Handler

  1. newCachedThreadPool

    • 建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。

    • ****線程池為無限大****

  2. newFixedThreadPool

    • 可控制線程最大并發數,超出的線程會在隊列中等待。
  3. newScheduledThreadPool

    • 支持定時,延遲及周期性任務執行
  4. newSingleThreadExecutor

    • 它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行

app保活

保活:監聽SCREEN_ON/OFF廣播,啟動1px的透明Activity; 啟動空通知,提高fg-service; 申請權限,加入白名單

Activity Bundle

  • Bundle可對對象進行操作,而Intent是不可以。Bundle相對于Intent擁有更多的接口,用起來比較靈活,但是使用Bundle也還是需要借助Intent才可以完成數據傳遞總之,Bundle旨在存儲數據,而Intent旨在傳值。

  • 為什么Bundle不直接使用Hashmap代替呢?

      • Bundle內部是由ArrayMap實現的,ArrayMap的內部實現是兩個數組,一個int數組是存儲對象數據對應下標,一個對象數組保存key和value,內部使用二分法對key進行排序,所以在添加、刪除、查找數據的時候,都會使用二分法查找,只適合于小數據量操作,如果在數據量比較大的情況下,那么它的性能將退化。而HashMap內部則是數組+鏈表結構,所以在數據量較少的時候,HashMap的Entry Array比ArrayMap占用更多的內存。因為使用Bundle的場景大多數為小數據量,我沒見過在兩個Activity之間傳遞10個以上數據的場景,所以相比之下,在這種情況下使用ArrayMap保存數據,在操作速度和內存占用上都具有優勢,因此使用Bundle來傳遞數據,可以保證更快的速度和更少的內存占用。
      • 另外一個原因,則是在Android中如果使用Intent來攜帶數據的話,需要數據是基本類型或者是可序列化類型,HashMap使用Serializable進行序列化,而Bundle則是使用Parcelable進行序列化。而在Android平臺中,更推薦使用Parcelable實現序列化,雖然寫法復雜,但是開銷更小,所以為了更加快速的進行數據的序列化和反序列化,系統封裝了Bundle類,方便我們進行數據的傳輸。
  • Activity/Fragment狀態數據的保存與恢復

  • 消息機制中的Message的setData

LeakCanary v2

  1. 先注冊監聽觀察Activity/Fragment

  2. 在Activity destroy后將Activity的弱引用關聯到ReferenceQueue中,這樣Activity將要被GC前,會出現在ReferenceQueue中。

  3. 隨后,會向主線程的MessageQueue添加一個IdleHandler,用于在idle時觸發一個發生在HandlerThread的等待5秒后開始檢測內存泄漏的代碼。 這段代碼首先會判斷是否對象出現在引用隊列中,如果有,則說明沒有內存泄漏,結束。否則,調用Runtime.getRuntime().gc()進行GC,等待100ms后再次判斷是否已經出現在引用隊列中,若還沒有被出現,那么說明有內存泄漏,開始dump hprof。

Service

在后臺執行長時間運行操作而沒有用戶界面的應用組件

  • 啟動狀態

    • startService()后臺無限期運行,即使啟動服務的組件已被銷毀也不受影響,除非手動調用才能停止服務, 已啟動的服務通常是執行單一操作,而且不會將結果返回給調用方。
  • 綁定狀態

    • bindService()綁定服務提供了一個客戶端-服務器接口,允許組件與服務進行交互、發送請求、獲取結果,甚至是利用進程間通信 (IPC) 跨進程執行這些操作。 僅當與另一個應用組件綁定時,綁定服務才會運行。 多個組件可以同時綁定到該服務,但全部取消綁定后,該服務即會被銷毀

    • 先綁定服務后啟動服務 則宿主(Activity)被銷毀了,也不會影響服務的運行

    • 先啟動服務后綁定服務 則解除綁定后,服務依然按啟動服務的生命周期在后臺運行,直到有Context調用了stopService()或是服務本身調用了stopSelf()方法抑或內存不足時才會銷毀服務

  • onBind() 當另一個組件想通過調用 bindService() 與服務綁定(例如執行 RPC)時,系統將調用此方法。在此方法的實現中,必須返回 一個IBinder 接口的實現類,供客戶端用來與服務進行通信。無論是啟動狀態還是綁定狀態,此方法必須重寫,但在啟動狀態的情況下直接返回 null。

  • onCreate() 首次創建服務時,系統將調用此方法來執行一次性設置程序(在調用 onStartCommand() 或onBind() 之前)。如果服務已在運行,則不會調用此方法,該方法只調用一次

  • onStartCommand() 當另一個組件(如 Activity)通過調用 startService() 請求啟動服務時,系統將調用此方法。一旦執行此方法,服務即會啟動并可在后臺無限期運行。 如果自己實現此方法,則需要在服務工作完成后,通過調用 stopSelf() 或 stopService() 來停止服務。(在綁定狀態下,無需實現此方法。)

    • intent :啟動時,啟動組件傳遞過來的Intent,如Activity可利用Intent封裝所需要的參數并傳遞給Service

    • flags:表示啟動請求時是否有額外數據,可選值有 0,START_FLAG_REDELIVERY,START_FLAG_RETRY,0代表沒有,它們具體含義如下:

      START_FLAG_REDELIVERY 這個值代表了onStartCommand方法的返回值為 START_REDELIVER_INTENT,而且在上一次服務被殺死前會去調用stopSelf方法停止服務。其中START_REDELIVER_INTENT意味著當Service因內存不足而被系統kill后,則會重建服務,并通過傳遞給服務的最后一個 Intent 調用 onStartCommand(),此時Intent時有值的。

      START_FLAG_RETRY 該flag代表當onStartCommand調用后一直沒有返回值時,會嘗試重新去調用onStartCommand()

    • startId : 指明當前服務的唯一ID,與stopSelfResult (int startId)配合使用,stopSelfResult 可以更安全地根據ID停止服務。

    • 實際上onStartCommand的返回值int類型才是最最值得注意的,它有三種可選值, START_STICKY,START_NOT_STICKY,START_REDELIVER_INTENT,它們具體含義如下:

      START_STICKY ??當Service因內存不足而被系統kill后,一段時間后內存再次空閑時,系統將會嘗試重新創建此Service,一旦創建成功后將回調onStartCommand方法,但其中的Intent將是null,除非有掛起的Intent,如pendingintent,這個狀態下比較適用于不執行命令、但無限期運行并等待作業的媒體播放器或類似服務。

      START_NOT_STICKY ??當Service因內存不足而被系統kill后,即使系統內存再次空閑時,系統也不會嘗試重新創建此Service。除非程序中再次調用startService啟動此Service,這是最安全的選項,可以避免在不必要時以及應用能夠輕松重啟所有未完成的作業時運行服務。

      START_REDELIVER_INTENT ??當Service因內存不足而被系統kill后,則會重建服務,并通過傳遞給服務的最后一個 Intent 調用 onStartCommand(),任何掛起 Intent均依次傳遞。與START_STICKY不同的是,其中的傳遞的Intent將是非空,是最后一次調用startService中的intent。這個值適用于主動執行應該立即恢復的作業(例如下載文件)的服務。

  • onDestroy() 當服務不再使用且將被銷毀時,系統將調用此方法。服務應該實現此方法來清理所有資源,如線程、注冊的偵聽器、接收器等,這是服務接收的最后一個調用

  • [圖片上傳失敗...(image-48132c-1682881853345)]

前臺服務

通知欄Notification

后臺服務

后臺服務優先級相對比較低,當系統出現內存不足的情況下,它就有可能會被回收掉

IntentService

IntentService是專門用來解決Service中不能執行耗時操作這一問題的

系統服務

broadcastreceiver 監視系統服務

廣播

BroadcastReceiver

  • 普通廣播(Normal Broadcast)

  • 系統廣播(System Broadcast)

  • 有序廣播(Ordered Broadcast)

    • 接收廣播按順序接收

    • 先接收的廣播接收者可以對廣播進行截斷,即后接收的廣播接收者不再接收到此廣播;

    • 先接收的廣播接收者可以對廣播進行修改,那么后接收的廣播接收者將接收到被修改后的廣播

  • 粘性廣播(Sticky Broadcast)

  • App應用內廣播(Local Broadcast)

  • 特別注意

    • 對于靜態注冊(全局+應用內廣播),回調onReceive(context, intent)中的context返回值是:ReceiverRestrictedContext;

    • 對于全局廣播的動態注冊,回調onReceive(context, intent)中的context返回值是:Activity Context;

    • 對于應用內廣播的動態注冊(LocalBroadcastManager方式),回調onReceive(context, intent)中的context返回值是:Application Context。

    • 對于應用內廣播的動態注冊(非LocalBroadcastManager方式),回調onReceive(context, intent)中的context返回值是:Activity Context;

  • 本地廣播和全局廣播的區別

    • 本地廣播

      • 廣播事件的發送和接收都在本應用,不影響其他應用也不受其他應用影響,只能被動態注冊,不能靜態注冊,主要用法都在LocalBroadcastManager類中
    • 全局廣播

      • 可以接收其他應用發的廣播,也可以發送廣播讓其他應用接收,全局廣播既可以動態注冊,也可以靜態注冊,接受其他應用和系統廣播是全局廣播的一個重要應用點。總體來說兩者應用場景不同

LiveData

  • 觀察者對象必須得處于主線程中

    • setValue()方法相信大家都很熟悉,我們必須在主線程中進行調用,否則會拋出異常。在代碼中體現出來的,正是通過assertMainThread("setValue");來保證方法處于主線程中

    • postValue用于在子線程通知主線程livedata發生了變化

  • 專用于 Android 的具備自主生命周期感知能力的可觀察的數據存儲器類

  • newFixThreadPool(2)

  • 通過對比ObserverWrapperLiveData之間的version,來判斷是否更新

Databing\ViewModel

  • 綁定的方式分兩種

    • 單向綁定 直接在xml中寫對應的參數名 android:text="@{userInfo.name}"

    • 雙向綁定 android:text="@={userInfo.nickName}"

  • 原理

    1. 首先通過DataBindingUtil.setContentView()來查找對應的布局

    2. 然后生成一個全新的tag值并且賦值給每個view

    3. 進行臟標記

    4. 注冊監聽

jni

普通應用

java->jni: .so/.dll

jni->java:

  1. jclass cls = (*env)->FindClass(env, "jni/test/Demo"); //把點號換成斜杠

  2. jclass cls = (*env)-> GetObjectClass(env, obj); //其中obj是要引用的對象,類型是jobject

  3. Call<TYPE>Method或者 CallStatic<TYPE>Method(訪問類的靜態方法)

  4. jfieldID (JNICALL *GetFieldID) (JNIEnv *env, jclass clazz, const char *name, const char *sig); jfieldID (JNICALL *GetStaticFieldID) (JNIEnv *env, jclass clazz, const char *name, const char *sig);

  5. (*env)->SetObjectField(env, jobj, fid, new_jstr);

直播

音頻

  1. AudioRecord

  2. 單雙聲道

  3. 采樣率

  4. 緩沖區

  5. 單幀大小計算

  6. p幀(與前一幀的差值,較小) i幀(關鍵幀,較大),B幀(是前一幀及后一幀的差別,但解碼麻煩,需要先知道前一幀和后一幀)

  7. webrtc - (ns降噪模塊,vad語音端點識別模塊-可用于人聲識別,aecm回聲消除模塊)

    1. 幀間隔毫秒必須是80的倍數

    2. 幀大小必須是160的倍數

視頻

  1. 編碼

  2. 攝像頭(攝像頭數,前后,采集高寬,閃光燈,對焦方式)

  3. 預覽幀數

  4. 縮放和對焦

  5. 軟/硬解碼

  6. h264/h265

    1. 壓縮比接近50%

    2. 解碼cpu負擔大

    3. 減少實時的時延、減少信道獲取時間和隨機接入時延、降低復雜度

  7. ijkplayer

  8. ffmepg

    • 音視頻解碼器
  9. yuv

    • 負責處理圖像顏色

推流

推薦:RTMP RTSP

兩者區別:

[圖片上傳失敗...(image-1cc5df-1682881853347)]

注: RTP傳輸是指實時傳輸協議,是建立在udp協議之上

udp與tcp區別

UDP TCP
是否連接 無連接 面向連接
是否可靠 不可靠傳輸,不使用流量控制和擁塞控制 可靠傳輸,使用流量控制和擁塞控制
連接對象個數 支持一對一,一對多,多對一和多對多交互通信 只能是一對一通信
傳輸方式 面向報文 面向字節流
首部開銷 首部開銷小,僅8字節 首部最小20字節,最大60字節
適用場景 適用于實時應用(IP電話、視頻會議、直播等) 適用于要求可靠傳輸的應用,例如文件傳輸

IM

  • 千人群優化

    • 消息合并插入

      • 將單一一條消息合并成一個消息塊進行傳輸
    • 并發限制(?)

      • 當進入消息未讀較多的群時,分頁

內存

  1. 內存泄漏 Memory Leak: 本該回收的對象不能被回收而停留在堆內存中,從而產生了內存泄漏。

  2. 內存溢出 Out Of Memory: 內存溢出是指APP向系統申請超過最大閥值的內存請求

強引用、軟引用、弱引用和虛引用

強引用 垃圾回收器絕不會回收它
軟引用 內存空間充足時,垃圾回收器不會回收它;如果內存空間不足了,就會回收這些對象的內存
弱引用 不管當前內存空間足夠與否,都會回收它的內存,但不一定及時
虛引用 任何時候都可能被垃圾回收器回收

LruCache

LRU是近期最少使用的算法,它的核心思想是當緩存滿時,會優先淘汰那些近期最少使用的緩存對象

  • 原理是把最近使用的對象用強引用(即我們平常使用的對象引用方式)存儲在 LinkedHashMap 中。當緩存滿時,把最近最少使用的對象從內存中移除,并提供了get和put方法來完成緩存的獲取和添加操作。

  • LinkedHashMap是以訪問順序排序的

Bitmap的三級緩存

  1. 通過BitmapFactory.Options中的inJustDecodeBounds = true 不對bitmap進行實際的內存分配,但仍然可以獲得所有屬性

  2. 通過對比bitmap的實際大小和imageView的大小進行二次采樣inSimpleSize

  3. 使用LruCache

Parcelable和Serializable的區別和比較

Parcelable和Serializable都是實現序列化并且都可以用于Intent間傳遞數據,Serializable是Java的實現方式,可能會頻繁的IO操作,所以消耗比較大,但是實現方式簡單 Parcelable是Android提供的方式,效率比較高,但是實現起來復雜一些 , 二者的選取規則是:內存序列化上選擇Parcelable, 存儲到設備或者網絡傳輸上選擇Serializable(當然Parcelable也可以但是稍顯復雜)

啟動模式

standard 模式

這是默認模式,每次激活Activity時都會創建Activity實例,并放入任務棧中。使用場景:大多數Activity。

singleTop 模式

如果在任務的棧頂正好存在該Activity的實例,就重用該實例( 會調用實例的 onNewIntent() ),否則就會創建新的實例并放入棧頂,即使棧中已經存在該Activity的實例,只要不在棧頂,都會創建新的實例。使用場景如新聞類或者閱讀類App的內容頁面。

singleTask 模式

如果在棧中已經有該Activity的實例,就重用該實例(會調用實例的 onNewIntent() )。重用時,會讓該實例回到棧頂,因此在它上面的實例將會被移出棧。如果棧中不存在該實例,將會創建新的實例放入棧中。使用場景如瀏覽器的主界面。不管從多少個應用啟動瀏覽器,只會啟動主界面一次,其余情況都會走onNewIntent,并且會清空主界面上面的其他頁面。

singleInstance 模式

在一個新棧中創建該Activity的實例,并讓多個應用共享該棧中的該Activity實例。一旦該模式的Activity實例已經存在于某個棧中,任何應用再激活該Activity時都會重用該棧中的實例( 會調用實例的 onNewIntent() )。其效果相當于多個應用共享一個應用,不管誰激活該 Activity 都會進入同一個應用中。使用場景如鬧鈴提醒,將鬧鈴提醒與鬧鈴設置分離。singleInstance不要用于中間頁面,如果用于中間頁面,跳轉會有問題,比如:A -> B (singleInstance) -> C,完全退出后,在此啟動,首先打開的是B。

View和ViewGroup的繪制

  • 對于 ViewGroup,除了要完成自己的測量,還要遍歷調用子元素的 measure() 方法,而 View 只需要通過 measure() 方法就能確定測量規格

  • layout() 方法的作用是 ViewGroup 用于確定子元素的位置,當 ViewGroup 的位置確定后,會在 onLayout() 方法中遍歷所有子 View 并調用子 View 的 layout() 方法。

  • View 和 ViewGroup 沒有實現 onDraw() 方法,接下來就是 dispatchDraw() 方法,View 沒有實現這個方法

View ViewGroup的事件分發機制

點擊事件被攔截,但是相傳到下面的view

getParent().requestDisabllowInterceptTouchEvent(true)

  1. 表示事件是否會繼續分發出去,默認返回false,返回true時表示事件不會再繼續分發,甚至都不會分發到自身的onTouchEvent方法;

  2. dispatchTouchEvent 分發事件,當該方法返回true時,該View不會繼續分發事件,包括該事件不會繼續分發到該View的onInterceptTouchEvent方法和onTouchEvent方法;

  3. onInterceptTouchEvent 攔截事件的傳遞,是否會繼續向子View、子ViewGroup傳遞,當該方法返回true時,事件不會繼續向子View、子ViewGroup傳遞,相當于父級View把事件在此處截斷了;

  4. onTouchEvent 消費事件,對點擊事件做相應的點擊響應處理,具體執行點擊后的操作,如果子View不做處理,即返回false,該事件還會繼續傳遞到父View的onTouchEvent方法去處理,直到傳遞到組外層; 如果該方法返回true,表示這個事件被消費掉了,這個事件就此終止了,不會再有任何傳遞;

  5. Android事件分發是先傳遞到ViewGroup,再由ViewGroup傳遞到View的。

  6. 在ViewGroup中可以通過onInterceptTouchEvent方法對事件傳遞進行攔截,onInterceptTouchEvent方法返回true代表不允許事件繼續向子View傳遞,返回false代表不對事件進行攔截,默認返回false。

  7. 子View中如果將傳遞的事件消費掉,ViewGroup中將無法接收到任何事件

  8. [圖片上傳失敗...(image-e2672b-1682881853345)]

android與JS互動

  • android調用JS: WebView的loadUrl(); WebView的evaluateJavascript()

    • 被調用的Js方法是有返回值的,如果是采用loadUrl()調用,返回值也會用loadUrl()載入,直接顯示在WebView上,這顯然是不對的,我們只想隱形的接收返回值,而evaluateJavascript()就提供了這樣的隱形接收方式,不會調用到loadUrl()。
  • JS調用android: 通過WebView的addJavascriptInterface()進行對象映射; WebViewClient 的shouldOverrideUrlLoading ()方法回調攔截 url; WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回調攔截JS對話框alert()、confirm()、prompt() 消息

kotlin

  1. reified 使得泛型的方法假裝在運行時能夠獲取泛型的類信息

  2. 原來在Java中,類處于頂層,類包含屬性和方法,在Kotlin中,函數站在了類的位置,我們可以直接把函數放在代碼文件的頂層,讓它不從屬于任何類,可以通過import 包名.函數名來

  3. 導入我們將要使用的函數

    協程

    關鍵字 yield resume suspend

    總結下,協程是跑在線程上的,一個線程可以同時跑多個協程,每一個協程則代表一個耗時任務,我們手動控制多個協程之間的運行、切換,決定誰什么時候掛起,什么時候運行,什么時候喚醒,而不是 Thread 那樣交給系統內核來操作去競爭 CPU 時間片

    1. 協程其實就相當于回調,利用掛起函數來等待回調結果,不會阻塞線程

    2. 協程可以用來直接標記方法,由程序員自己實現切換,調度,不再采用傳統的時間段競爭機制

    3. launch - 創建協程 3個參數和返回值 Job:

      <pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="kotlin" cid="n545" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: "Fira Code", Consolas, "Lucida Console", Courier, monospace, "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(40, 44, 52); position: relative !important; padding: 6px 10px; box-shadow: rgba(0, 0, 0, 0.16) 0px 2px 5px 0px, rgba(0, 0, 0, 0.12) 0px 2px 10px 0px; margin-bottom: 2.5rem; border: none; border-radius: 6px; width: inherit;">launch(CoroutineContext,CoroutineStart,block): Job</pre>

      1. CoroutineContext - 可以理解為協程的上下文

        1. Dispatchers.Default

        2. Dispatchers.IO -

        3. Dispatchers.Main - 主線程

        4. Dispatchers.Unconfined - 沒指定,就是在當前線程

      2. CoroutineStart - 啟動模式,默認是DEAFAULT,也就是創建就啟動

        1. DEAFAULT - 模式模式,不寫就是默認

        2. ATOMIC -

        3. UNDISPATCHED

        4. LAZY - 懶加載模式,你需要它的時候,再調用啟動

      3. block - 閉包方法體,定義協程內需要執行的操作

      4. Job - 協程構建函數的返回值,可以把 Job 看成協程對象本身

        1. job.start() - 啟動協程,除了 lazy 模式,協程都不需要手動啟動

        2. job.join() - 等待協程執行完畢

        3. job.cancel() - 取消一個協程

        4. job.cancelAndJoin() - 等待協程執行完畢然后再取消

    4. async - 創建帶返回值的協程,返回的是 Deferred 類

      • async 同 launch 唯一的區別就是 async 是有返回值的

      • Deferred 繼承自 Job 接口,Job有的它都有,增加了一個方法 await ,這個方法接收的是 async 閉包中返回的值,async 的特點是不會阻塞當前線程,但會阻塞所在協程,也就是掛起

        但是注意啊,async 并不會阻塞線程,只是阻塞鎖調用的協程

    5. withContext - 不創建新的協程,在指定協程上運行代碼塊

    6. runBlocking - 不是 GlobalScope 的 API,可以獨立使用,區別是 runBlocking 里面的 delay 會阻塞線程,而 launch 創建的不會

      • 在 runBlocking 閉包里面啟動另外的協程,協程里面是可以嵌套啟動別的協程的
    7. 協程執行時, 協程和協程,協程和線程內代碼是順序運行的

    8. 協程掛起時,就不會執行了,而是等待掛起完成且線程空閑時才能繼續執行

    9. relay 和 yield 方法是協程內部的操作,可以掛起協程,區別是 relay 是掛起協程并經過執行時間恢復協程,當線程空閑時就會運行協程;yield 是掛起協程,讓協程放棄本次 cpu 執行機會讓給別的協程,當線程空閑時再次運行協程。

hook

glide

原理

[圖片上傳失敗...(image-b7389a-1682881853347)

  1. Glide.with(context)創建RequestManager

    • RequestManager負責管理當前context的所有Request

    • Context可以傳Fragment、Activity或者其他Context,當傳Fragment、Activity時,當前頁面對應的Activity的生命周期可以被RequestManager監控到,從而可以控制Request的pause、resume、clear。這其中采用的監控方法就是在當前activity中添加一個沒有view的fragment,這樣在activity發生onStart onStop onDestroy的時候,會觸發此fragment的onStart onStop onDestroy。

    • RequestManager用來跟蹤眾多當前頁面的Request的是RequestTracker類,用弱引用來保存運行中的Request,用強引用來保存暫停需要恢復的Request。

  2. Glide.with(context).load(url)創建需要的Request

    • 通常是DrawableTypeRequest,后面可以添加transform、fitCenter、animate、placeholder、error、override、skipMemoryCache、signature等等

    • 如果需要進行Resource的轉化比如轉化為Byte數組等需要,可以加asBitmap來更改為BitmapTypeRequest

    • Request是Glide加載圖片的執行單位

  3. Glide.with(context).load(url).into(imageview)

    • 在Request的into方法中會調用Request的begin方法開始執行

    • 在正式生成EngineJob放入Engine中執行之前,如果并沒有事先調用override(width, height)來指定所需要寬高,Glide則會嘗試去獲取imageview的寬和高,如果當前imageview并沒有初始化完畢取不到高寬,Glide會通過view的ViewTreeObserver來等View初始化完畢之后再獲取寬高再進行下一步

優點

  1. 指定圖片大小

    • 自動判斷imageView大小,然后只加載等大的像素,而不會全部加載進imageView
  2. 方便圖片格式的切換

  3. 方便自定義圖片的裁剪等轉換(BitmapTransformation

Retrefit

本質上 Retrofit 是一個對 OkHttp 進行進一步封裝的框架

原理

  • 通過 addConverterFactoryaddCallAdapterFactory 進行數據格式的二次/自定義封裝

  • 利用MainThreadExecutor來進行線程之間的切換

  • 核心思想是將 http 請求過程抽象成了一個對象 ServiceMethod, 這個對象的構造的時候,會通過 java 反射的方式傳入一個 method 對象,而這個對象就是我們在 interface 中定義的請求方法

RxJava

常用操作符

  • map 轉換事件,返回普通事件

  • flatMap 轉換事件,返回` Observable

  • conactMap concatMap 與 FlatMap 的唯一區別就是 concatMap 保證了順序

  • subscribeOn 規定被觀察者所在的線程

  • observeOn 規定下面要執行的消費者所在的線程

  • take 接受一個 long 型參數 count ,代表至多接收 count 個數據

  • debounce 去除發送頻率過快的項,常用在重復點擊解決上,配合 RxBinging 使用效果很好

  • timer 定時任務,多少時間以后發送事件

  • interval 每隔一定時間執行一些任務

  • skip 跳過前多少個事件

  • distinct 去重

  • takeUntil 直到到一定條件的是停下,也可以接受另外一個被觀察者,當這個被觀察者結束之后則停止第一個被觀察者

  • Zip 專用于合并事件,該合并不是連接(連接操作符后面會說),而是兩兩配對,也就意味著,最終配對出的 Observable 發射事件數目只和少的那個相同。不影響Observable的發射,Observable 被觀察者會一直發射,不會停,只是Observer 接收不到

  • merge 多個 Observable 發射的數據隨機發射,不保證先后順序

  • Concat 多個 Observable 組合以后按照順序發射,保證了先后順序,不過最多能組合4個 Observable ,多的可以使用 contactArray

  • onErrorReturn 遇到錯誤是發射指定的數據到 onNext,并正常終止

  • onErrorResumeReturn 遇到錯誤時,發射設置好的一個 Observable ,用來發送數據到 onNext,并正常終止

  • onExceptionResumeReturn 和onErrorResumeReturn 類似,不同之處在于會判斷是否是 Exception。如果是和 onErrorResumeReturn 一樣,不是則會調用 onError。不會調用onNext

Map和flatMap的區別

  • 前者是嚴格按照1.2.3.4.5順序發的,經過map以后還是按照這個順序

  • 后者是1.2.3.4.5發送完到 flatMap 里面,然后經過flatmap進行組裝以后再發出來,順序可能會打亂

  • 使用 contactMap 可以保證轉換后的事件發射順序。

[圖片上傳失敗...(image-db706b-1682881853

[圖片上傳失敗...(image-bf9436-1682881853347)]

跨進程

[圖片上傳失敗...(image-96a68e-1682881853347)]

AIDL

利用了liunx下的Binder機制 IPC

Linux現有的進程通信手段有以下幾種:

  • 管道:在創建時分配一個page大小的內存,緩存區大小比較有限;

  • 消息隊列:信息復制兩次,額外的CPU消耗;不合適頻繁或信息量大的通信;

  • 共享內存:無須復制,共享緩沖區直接附加到進程虛擬地址空間,速度快;但進程間的同步問題操作系統無法實現,必須各進程利用同步工具解決;

  • 套接字:作為更通用的接口,傳輸效率低,主要用于不同機器或跨網絡的通信;

  • 信號量:常作為一種鎖機制,防止某進程正在訪問共享資源時,其他進程也訪問該資源。因此,主要作為進程間以及同一進程內不同線程之間的同步手段。 不適用于信息交換,更適用于進程中斷控制,比如非法內存訪問,殺死某個進程等;

Binder來說,數據從發送方的緩存區拷貝到內核的緩存區,而接收方的緩存區與內核的緩存區是映射到同一塊物理地址的 ,節省了一次數據拷貝的過程,如圖:

img

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

  • 首先 Binder 驅動在內核空間創建一個數據接收緩存區。

  • 接著在內核空間開辟一塊內核緩存區,建立內核緩存區和內核中數據接收緩存區之間的映射關系,以及內核中數據接收緩存區和接收進程用戶空間地址的映射關系。

  • 發送方進程通過系統調用 copyfromuser() 將數據 copy 到內核中的內核緩存區,由于內核緩存區和接收進程的用戶空間存在內存映射,因此也就相當于把數據發送到了接收進程的用戶空間,這樣便完成了一次進程間的通信。

img
img

ContentProvider

通過 ContentResolver 來訪問 ContentProvider 提供的數據,而 ContentResolver 通過 uri 來定位自己要訪問的數據

  • public boolean **onCreate()**:在創建 ContentProvider 時使用

  • public Cursor **query()**:用于查詢指定 uri 的數據返回一個 Cursor

  • public Uri **insert():**用于向指定uri的 ContentProvider 中添加數據

  • public int **delete()**:用于刪除指定 uri 的數據

  • public int **update()**:用戶更新指定 uri 的數據

  • public String **getType()**:用于返回指定的 Uri 中的數據 MIME 類型

  • 數據源可能是數據庫,也可以是文件、xml或網絡等其他存儲方式。

動畫

屬性動畫

  • ValueAnimator

    • 無法像ObjectAnimator一樣直接作用于對象,只能通過添加監聽,獲取動畫過程之,然后手動設置給對象改變對象的屬性
  • ObjectAnimator

補間動畫TweenAnimation

逐幀動畫FrameAnimation

滑動時不加載圖片

通過RecyclerView的滑動狀態改變,來對glide的圖片加載請求的暫停/恢復請求

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="java" cid="n783" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: "Fira Code", Consolas, "Lucida Console", Courier, monospace, "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(40, 44, 52); position: relative !important; padding: 6px 10px; box-shadow: rgba(0, 0, 0, 0.16) 0px 2px 5px 0px, rgba(0, 0, 0, 0.12) 0px 2px 10px 0px; margin-bottom: 2.5rem; border: none; border-radius: 6px; width: inherit; color: rgb(40, 44, 52); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"> mRecycleView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
if (getActivity() != null){
Glide.with(getActivity()).resumeRequests();//恢復Glide加載圖片
}
}else {
if (getActivity() != null){
Glide.with(getActivity()).pauseRequests();//禁止Glide加載圖片
}
}
}
});</pre>

RecyclerView

  • 四級緩存

    • mAttachedScrap:緩存屏幕中可見范圍中的ViewHolder。

      • mAttachedScrap緩存的是當前屏幕上的ViewHolder,對應的數據結構是ArrayList,沒有大小限制。在調用LayoutManager#onLayoutChildren方法時對views進行布局

      • 特性是:如果和RecyclerView上的position或者itemId匹配上了,那么就可以直接拿來使用,不需要調用onBindViewHolder重新綁定數據

    • mCachedViews :緩存滑動中即將與RecyclerView分離的ViewHolder

      • mCachedViews緩存滑動時即將與RecyclerView分離的ViewHolder,其數據結構為ArrayList,該緩存對大小是有限制的,默認為2個

      • 該緩存中的ViewHolder的特性是:只要position和itemId匹配上了,則可直接使用,不需要調用onBindViewHolder重新綁定數據。

    • mViewCacheExtension:自定義實現的緩存。

    • mRecyclerPool:ViewHolder緩存池,可支持不同的ViewType。

      • 本質上是一個SparseArray,其中key是ViewType,value是ArrayList<ViewHolder>,默認每個ArrayList中最多存儲5個

      • ViewHolder存儲在緩存池的前會進行重置變成一個干凈的ViewHolder,所以在復用時,需要調用onBindViewHolder重綁數據。

  • 復用流程

    • 復用肯定是在填充子元素過程中完成的

    • 先通過getChangedScrapViewForPosition

      • notifyItemChanged()方法,數據發生變化時,item緩存在mChangedScrap和mAttachedScrap中,后續拿到的ViewHolder需要重新綁定數據。此時查找ViewHolder就會通過position和id分別在scrap的mChangedScrap中查找
    • 然后getScrapOrHiddenOrCachedHolderForPosition

      • 沒有找到視圖,根據position分別在scrap的mAttachedScrap、mHiddenViews、mCachedViews中查找
    • 再然后RecycledViewPool

    • 最后創建新的ViewHolder

Context

img

首先什么是Context

Context 相當于 Application 的大管家,主要負責:

  • 四大組件的交互,包括啟動 Activity、Broadcast、Service,獲取 ContentResolver 等

  • 獲取系統/應用資源,包括 AssetManager、PackageManager、Resources、System Service 以及 color、string、drawable 等文件,包括獲取緩存文件夾、刪除文件、SharedPreference 相關等 數據庫(SQLite)相關,包括打開數據庫、刪除數據庫、獲取數據庫路徑等

  • 其它輔助功能,比如設置 ComponentCallbacks,即監聽配置信息改變、內存不足等事件的發生

如果把整個App當作一個內存塊,那context相當于一個指針

Context實例個數 = Service個數 + Activity個數 + 1(Application對應的Context實例)

Context的幾種類型

  1. ContextWrapper、ContextThemeWrapper、ContextImpl 的區別:

    • ContextWrapper、ContextThemeWrapper 都是 Context 的代理類,二者的區別在于 ContextThemeWrapper 有自己的 Theme 以及 Resource,并且 Resource 可以傳入自己的配置初始化

    • ContextImpl 是 Context 的主要實現類,Activity、Service 和 Application 的 BaseContext 都是由它創建的,即 ContextWrapper 代理的就是 ContextImpl 對象本身

    • ContextImpl 和 ContextThemeWrapper 的主要區別是, ContextThemeWrapper 有 Configuration 對象,Resource 可以根據這個對象來初始化

  2. Activity、Service 和 Application 的 Base Context 都是由 ContextImpl 創建的,且創建的都是 ContextImpl 對象,即它們都是 ContextImpl 的代理類 Service 和 Application 使用同一個 Recource,和 Activity 使用的 Resource 不同

getApplicationContext 返回的就是 Application 對象本身,一般情況下它對應的是應用本身的 Application 對象。

baseContext和applicationContext的區別

  • getApplicationContext() 是返回應用的上下文,也就是把Application作為Context,生命周期是整個應用,應用摧毀它才摧毀

  • Activity的Context,Activity.this的context 返回當前Activity的上下文,及把Activity用作Context,生命周期屬于Activity ,Activity 摧毀他就摧毀

  • 通常不會建議使用baseContext,是因為baseContext是具體的系統生產的對象,其中不包含了具體用戶的一些定制信息和內容,所以通過baseContext獲取的Resource/Theme是會與預期的不符。

ClassLoader及類加載機制

Android中ClassLoader的種類&特點

  • BootClassLoader(Java的BootStrap ClassLoader): 用于加載Android Framework層class文件。

  • PathClassLoader(Java的App ClassLoader): 用于加載已經安裝到系統中的apk中的class文件。

  • DexClassLoader(Java的Custom ClassLoader): 用于加載指定目錄中的class文件。

    1. PathClassLoader :只能加載已經安裝到Android系統中的apk文件(/data/app目錄),是 Android默認使用的類加載器.

    2. DexClassLoader :可以加載任意目錄下的dex/jar/apk/zip文件,比 PathClassLoader 更靈活,是 實現熱修復的重點。

  • BaseDexClassLoader: 是PathClassLoader和DexClassLoader的父類。

各個ClassLoader的加載順序

首先是BootClassLoader在Jvm剛起動的時候去加載java核心API的

然后是PathClassLoader/DexClassLoader加載Apk的應用類(熱修復,也就是向PathClassLoader的dexElements進行插入新的dex)

最后是CustomerClassLoader加載自定義的class文件

需要注意CLASS_ISPREVERIFIED 標記

[圖片上傳失敗...(image-2c4c8b-1682881853346

雙親委托模式

什么是雙親委托模式

當某個類加載器需要加載某個.class文件時,它首先把這個任務委托給他的上級類加載器,遞歸這個操作,如果上級的類加載器沒有加載,自己才會去加載這個類。

流程圖

img

作用

  1. 防止重復加載同一個.class。通過委托去向上面問一問,加載過了,就不用再加載一遍。保證數據安全。

  2. 保證核心.class不能被篡改。通過委托方式,不會去篡改核心.clas,即使篡改也不會去加載,即使加載也不會是同一個.class對象了。不同的加載器加載同一個.class也不是同一個Class對象。這樣保證了Class執行安全。

插件化

DexClassLoader去加載外部的apk

  • DexClassLoader重載了findClass方法,在加載類時會調用其內部的DexPathList去加載

  • 插件調用主工程

    • 在構造插件的ClassLoader時會傳入主工程的ClassLoader作為父加載器,所以插件是可以直接可以通過類名引用主工程的類
  • 主工程調用插件

    • 若使用多ClassLoader機制,主工程引用插件中類需要先通過插件的ClassLoader加載該類再通過反射調用其方法。插件化框架一般會通過統一的入口去管理對各個插件中類的訪問,并且做一定的限制。

    • 若使用單ClassLoader機制,主工程則可以直接通過類名去訪問插件中的類。該方式有個弊病,若兩個不同的插件工程引用了一個庫的不同版本,則程序可能會出錯,所以要通過一些規范去避免該情況發生。

熱修復

熱修復的原理就是將補丁 dex 文件放到 dexElements 數組靠前位置,這樣在加載 class 時,優先找到補丁包中的 dex 文件,加載到 class 之后就不再尋找,從而原來的 apk 文件中同名的類就不會再使用,從而達到修復的目的

  • Android SDK給我們單獨提供了dex打包工具d8

dex插件

先將修改了的類進行打包成dex包,在將dex進行加載,插入到dexElements集合的前面即可。而打包流程是先將.java文件編譯成.class文件,然后使用SDK工具打包成dex文件并發布到遠程服務端,然后APP端請求下載

apk插件

重新打了一個新的apk包作為插件,打包很簡單方便,缺點就是文件大。使用apk的話就沒必要是將dex插入dexElements里面去,直接將之前的dexElements替換就可以了

修復資源

  • 反射創建新的 AssetManager 對象,反射調用 addAssetPath 方法加載外部的資源。

  • 將 AssetManager 類型的 mAssets 字段的引用全部替換為新創建的 AssetManager 對象。

組件化

采用接口 + 實現的結構。每個組件聲明自己提供的服務 Service,這些 Service 都是一些抽象類或者接口,組件負責將這些 Service 實現并注冊到一個統一的路由 Router 中去。如果要使用某個組件的功能,只需要向 Router 請求這個 Service 的實現,具體的實現細節我們全然不關心,只要能返回我們需要的結果就可以了。這與 Binder 的 C/S 架構很相像。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容