<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Android 端內(nèi)數(shù)據(jù)狀態(tài)同步方案VM-Mapping

          共 5853字,需瀏覽 12分鐘

           ·

          2021-07-17 03:47

          背景

          西瓜在feed、詳情頁、個人主頁有一塊功能區(qū),包括了點贊、收藏、關(guān)注等功能。這些功能分散在很多場景。例如:

          長久以來對這些功能的處理都是孤立的:在以往的業(yè)務(wù)迭代中,都是業(yè)務(wù)A有了需求,就加個點贊的請求,把自己業(yè)務(wù)模塊的UI更新下就完事了,業(yè)務(wù)B也自己搞一下。當西瓜開始從切面發(fā)力互動業(yè)務(wù)的時候,這些問題就凸顯出來了。線上出現(xiàn)了很多在頁面A點贊完一個視頻到頁面B點贊狀態(tài)或者點贊數(shù)不對的case。

          問題拆解

          在分析這塊業(yè)務(wù)時,梳理出幾種問題:

          1. 業(yè)務(wù)上場景太分散,體現(xiàn)到代碼上就是在activity、scene、viewholder、自定義view等各種各樣的容器,多個業(yè)務(wù)模塊、多個端(web、flutter)上都有很相似的操作,代碼跨度很大。

          2. 存量的代碼中有些場景是處理過同步問題的,但是處理的又不徹底,方案也不一樣,比如有的情況用了全局注冊callback,來通知所有對結(jié)果敏感的場景;有的情況用了Eventbus;有的情況是更新內(nèi)存,但是卻只是個別幾個模塊通用。

          3. 一部分問題是原來的業(yè)務(wù)邏輯,比如,使用更新后的內(nèi)存變量在多個頁面或者模塊傳遞引用,由于層次比較深引用值被中間的流程篡改。

          4. 一部分問題是服務(wù)端數(shù)據(jù)邏輯問題。

          其中3、4點問題更像是邏輯bug。

          多個端的數(shù)據(jù)同步可以通過跨端事件,每個端收到事件后更新自己就行。所以最復(fù)雜最難搞的問題就是端內(nèi)多場景下的數(shù)據(jù)狀態(tài)同步問題。

          端內(nèi)問題聚焦在幾個case:

          case1: 普通頁面,如Activity or Fragment上的狀態(tài)同步;

          case2: feed卡片的狀態(tài)同步;

          case3: feed卡片內(nèi)多個復(fù)雜層級之間的狀態(tài)同步;

          case4: 以上的組合。

          目標

          • 數(shù)據(jù)狀態(tài)同步,是要保證兩個一致性:數(shù)據(jù)一致性、UI一致性;

          • 方案要使用簡單,理解簡單;

          • 盡可能減少性能開銷。

          方案調(diào)研

          EventBus

          這個方案的本質(zhì)是:監(jiān)聽者收到事件->更新UI/更新數(shù)據(jù)Model

          • 對于case1: 如果是A頁面發(fā)起,B頁面被動接收,只需要在B頁面接收事件,更新B頁面的Model對象+UI即可。但是在收到事件之后,一定要把當前頁面的model對象更新,不然會有不一致的問題。

          • 對于case2:

            • eventbus注冊在ViewHolder 上:由于ViewHolder的復(fù)用,ViewHolder的數(shù)量是少于“ListData”的,那么意味著,只在ViewHolder上監(jiān)聽,會出現(xiàn)那些沒有和ViewHolder 建立聯(lián)系的數(shù)據(jù)無法被更新到。如果使用黏性事件,該事件會一直在內(nèi)存中,粘性事件的膨脹不可控,很可能會造成嚴重的內(nèi)存問題。

            • eventbus注冊在Activity or 其它頁面上,收到事件后,遍歷數(shù)據(jù)列表,更新,然后通過RecyclerView的onDataItemChanged方法局部更新。但是在很多場景,比如西瓜feed,feed框架之下的view層次非常深。很多時候Rd只關(guān)注某類卡片下的某個UI組件,F(xiàn)eed框架和頂層頁面容器離的很遠,修改成本高,容易出錯,對feed框架或者頂層容器的侵入比較大。另外,onDataItemChanged的局部更新是ViewHolder 對應(yīng)的itemView的,這個維度比較大,并不能刷新單獨的一個點贊按鈕。

          基于k-v的監(jiān)聽、通知

          以對象id為key,某個屬性值如點贊數(shù)為value。事件發(fā)生時,將修改值寫入k-v列表,監(jiān)聽者全部監(jiān)聽這個變化。當新進入一個場景時,查詢k-v列表作為最新值。這個方案和Eventbus粘性事件很像。

          • k-v 粒度太細,一直在內(nèi)存中,非常容易膨脹,沒有合適的釋放時機,導致內(nèi)存浪費;一旦移除,就可能概率的數(shù)據(jù)同步失效。

          • k-v列表內(nèi)的狀態(tài)要使用者在合適的時機同步到業(yè)務(wù)層數(shù)據(jù)Model。

          全局共享數(shù)據(jù)Model實例

          同一個數(shù)據(jù)Model對象,比如一個卡片Model,每次更新都是全局可見的。但是很明顯:

          • 對數(shù)據(jù)Model的要求很高。一個業(yè)務(wù)層數(shù)據(jù)Model類型,要全局統(tǒng)一,比如,一個視頻卡片業(yè)務(wù)層的類型是“ModelA”,那么全局場景不能有“ModelB”表示卡片。在很多場景下,業(yè)務(wù)層會對原始數(shù)據(jù)Model進行包裝適配;

          • 內(nèi)存占用很大;可能要緩存很多個列表。

          基于注解的對象映射方案VM-Mapping

          特點

          1. 以命名空間+指定字段值 為key,匹配相同注解名的字段的映射,打平了Model類型的不同、層級嵌套的約束;

          2. 直接更新結(jié)果到數(shù)據(jù)model(如article),與數(shù)據(jù)model視角的同步;

          3. 打平了多個頁面、復(fù)雜view層級嵌套的差異;

          4. 自動處理更新,使用者僅需要關(guān)心怎么更新UI,不需要考慮數(shù)據(jù)Model的一致性;

          5. 任意場景的支持。

          思考

          1. 數(shù)據(jù)狀態(tài)同步,到底同步的是什么?

          2. 上述的方案中大致有幾個角色:事件、監(jiān)聽者、數(shù)據(jù)Model、UI。到底誰應(yīng)該是主導者?

          3. 基于事件的方案都需要把狀態(tài)同步給數(shù)據(jù)Model,能簡化嗎?

           這個過程中有四個角色,三個操作。

          突破View層級的限制

          從MVVM說起。MVVM是一種軟件設(shè)計典范,用一種業(yè)務(wù)邏輯、數(shù)據(jù)、界面顯示分離的方法組織代碼。

            MVVM本質(zhì)上是一種數(shù)據(jù)驅(qū)動UI的理念。從這個理念看,數(shù)據(jù)狀態(tài)同步,同步的是數(shù)據(jù)Model,UI的變更是由數(shù)據(jù)的變更引起的,真正關(guān)注的點應(yīng)該在數(shù)據(jù)本身上。 

           這樣,就不再需要額外一個接受事件的“容器”,來控制數(shù)據(jù)和UI了。到現(xiàn)在,只有三個角色,兩個操作了。

          再回過頭看,為什么跨頁面、跨多View層級很難找到一個通用方案,是因為總在找一個“容器”來承載事件的接受,然后再做雙份(數(shù)據(jù)和View)的同步。而且這個“容器”通常本身就是一個頁面,或者其它不同層級上的view,本身就存在很多樣化,為這種多樣化適配,就會讓事情變得復(fù)雜。

          假如不再找額外的“容器”,直接把監(jiān)聽綁定在數(shù)據(jù)上,那么View層級的限制也就不存在了。因為不管在什么場景,什么層級,真正的邏輯中心都是數(shù)據(jù),View也是通過數(shù)據(jù)渲染出來的,View不關(guān)心自己在什么層級,只關(guān)心數(shù)據(jù)的變化。

          突破類型的限制

          這里有幾個類型的限制:

          1. 數(shù)據(jù)Model的類型是否只能一成不變,假如網(wǎng)絡(luò)請求的原始數(shù)據(jù)是A類型,在場景1直接用了A類型,在場景2為了適配UI對A做了包裝:

          class A{
          val diggStatus : Int
          }
          class B {
          val a : A
          val showTipEnable : Boolean
          }

          雖然類型不同,但是對A、B來說,都是要更新diggStatus的;

          1. 在Android,數(shù)據(jù)Model的類型是強類型,是從網(wǎng)絡(luò)由二進制流反序列化出來的,那么同一個二進流,既可以反序列化成A類型,又可以反序列化成B類型,只要滿足反序列化規(guī)則就行。但是事實上,他們的業(yè)務(wù)本質(zhì)還是一個東西。

          class A{
          val diggStatus : Int
          }
          class B{
          val digg_status : Int
          }
          1. 事件本身也是一個數(shù)據(jù),只是它是用戶操作發(fā)起的,表象看和數(shù)據(jù)Model無關(guān),但是一個事件既然能更新某個數(shù)據(jù)Model,那他們一定存在著對應(yīng)關(guān)系。

          這個問題的本質(zhì)是,類型約束是語言特性,但是和業(yè)務(wù)屬性無關(guān),只要他們能確認是一個業(yè)務(wù)含義,不管他們怎么換“馬甲”,他們總是能匹配上的。

          這樣就演變成了:

          1. 怎么確定兩個類型是一個業(yè)務(wù)含義;

          2. 怎么確定屬性的對應(yīng)關(guān)系(字段匹配)。

          第一個好說,主要能有唯一的業(yè)務(wù)標識,就能確定是一個業(yè)務(wù)含義;怎么確定屬性的對應(yīng)關(guān)系呢?

          現(xiàn)有的技術(shù)體系里就有可以借鑒的思想:數(shù)據(jù)庫的使用。像jetpack 的Room組件:

          @Entity(tableName = "users")

          data class User(

          @PrimaryKey(autoGenerate
          = true) var userId: Long,

          @ColumnInfo(name = "user_name")var userName: String,

          @ColumnInfo(defaultValue = "china") var address: String

          )

          可以看到,我們只要要在應(yīng)用層這么定義一個數(shù)據(jù)Model叫User,為它加上注解,就可以把數(shù)據(jù)庫中的字段和我們的數(shù)據(jù)對應(yīng)上。那么方案呼之欲出,注解是可以完成屬性匹配的。

          于是乎整個流程就簡化成了:

           這個流程可以看到,只剩下了兩個角色,和兩個操作了。

          所謂數(shù)據(jù)驅(qū)動UI,就是View-Model;數(shù)據(jù)映射數(shù)據(jù),就是Data-Mapping,于是這個方案的名稱就是VM-Mapping。

          詳細設(shè)計

          需要對上述抽象流程做實現(xiàn)。

          映射

          前面說到,映射關(guān)系由注解維護,一個有三個注解:

          1. Mappable注解 :

          標注在class上,用來識別這個類是不是可以被處理。

          annotation class Mappable(val mappingSpaces: Arrary<String>)

          其中mappingSpace是命名空間,表示是“一類”數(shù)據(jù),可以和數(shù)據(jù)庫表名對比理解,mappingSpace就是tableName。

          1. PrimaryKey注解:

          標記在字段上,被標記的字段作為Model對象的唯一標識。

          mappingSpace+PrimaryKey的值,就是在映射關(guān)系中的唯一業(yè)務(wù)標識。

          @Target(AnnotationTarget.FIELD)

          @Retention(AnnotationRetention.RUNTIME)

          annotation class PrimaryKey
          1. MappableKey注解:

          標注在字段上,需要被映射對應(yīng)的字段

          @Target(AnnotationTarget.FIELD)

          @Retention(AnnotationRetention.RUNTIME)

          annotation class MappableKey(val value: String)

          映射關(guān)系說明: 

          數(shù)據(jù)驅(qū)動UI

          Android里有很多類似理念的東西,比如LiveData,就是數(shù)據(jù)更新通知到UI上。本質(zhì)上數(shù)據(jù)驅(qū)動UI,就是在數(shù)據(jù)Data<->UI 之間建一個“橋梁”。這個不過LiveData并不適合用在這里,理由是

          • LiveData綁定的生命周期是LifecycleOwner,也就是Activity、Fragment維度,明顯我們的場景維度更細;

          • 直接observeForever也可以,但是由于View層級的多樣,調(diào)用方通常需要合適的時機移除;

          • LiveData 強引用了數(shù)據(jù)Data,這個“橋梁”本身對數(shù)據(jù)Data的生命周期造成了影響。

          VM-Mapping做了個簡單方案。用了兩級HashMap,一級HashMap使用業(yè)務(wù)唯一標識(mappingSpace+PrimaryKey的值)為KEY,二級使用WeakHashMap,以數(shù)據(jù)Model實例為KEY,XGViewModel為VALUE。維護數(shù)據(jù)Data 和 UI回調(diào)之間的關(guān)系:

           XGViewModel維護了通知給UI的弱引用回調(diào)合集。一個數(shù)據(jù)Model實例對應(yīng)了一個XGViewModel。

          當映射發(fā)生時,會通過業(yè)務(wù)標識Key,查找所有還沒有被回收的數(shù)據(jù)Model實例,然后通過對應(yīng)的XGViewModel通知UI自己的變更。

          總體流程

           在這個流程中,業(yè)務(wù)使用只需要關(guān)心兩個問題,發(fā)起映射數(shù)據(jù)和更新視圖。因為存在列表,那么會有一個列表的維護者,就是所謂的映射中心。映射中心有兩個核心能力:

          1. 收集需要被更新的數(shù)據(jù)Model列表;

          2. 查找匹配。

          其它細節(jié)

          • 因為使用了反射,為了減少性能損耗,會對收集的數(shù)據(jù)Model類型做class和相關(guān)字段的緩存。

          • 列表存在膨脹現(xiàn)象,二級弱引用列表的key是數(shù)據(jù)Model實例本身,當它被虛擬機回收的時候,會把一級列表中的該項移除,當一級列表某個key下沒有內(nèi)容時,也會把該key移除。

            • 移除的時機在每次添加數(shù)據(jù)Model到列表;

            • 移除的條件是一級列表長度達到閾值。

          但是注意。這個移除并不會影響VM-Mapping的能力,因為VM-Mapping關(guān)注的是數(shù)據(jù)本身,當數(shù)據(jù)被回收的時候,不會有任何場景會用到這個數(shù)據(jù),自然也不用關(guān)心是不是需要通知到它。

          • 為了避免影響主線程,和多線程競爭列表的問題,映射中心操作都在單子線程中處理。

          方案對比

          方案優(yōu)勢劣勢
          Eventbus理解成本低事件、UI、數(shù)據(jù)Model三個角色都要保持一致,適配各種場景的成本高,不通用。
          全局共享數(shù)據(jù)Model實例使用簡單條件苛刻;占用內(nèi)存,膨脹不可控制。
          基于k-v的監(jiān)聽、通知各場景通用粒度太細導致內(nèi)存不可控制,移除策略會導致同步失效。事件需要手動同步數(shù)據(jù)Model。
          VM-Mapping使用簡單,不需要手動同步回數(shù)據(jù)Model,在所有場景下通用。用到了反射,有一部分性能損耗。

          方案收益

          西瓜在之前遺留了大量的類似問題,一直沒有好的方案解決,要么存在根本性缺陷,要么實施成本高。VM-Mapping支持了在西瓜中視頻相關(guān)的核心場景快速接入,實現(xiàn)了線上點贊數(shù)異常問題清零。

          后續(xù)計劃

          • 根據(jù)統(tǒng)計,由于使用運行時注解+反射,一個操作的耗時均值在10ms左右。仍然有可以優(yōu)化的空間。可以考慮使用編譯時注解維護數(shù)據(jù)映射關(guān)系。

          • 目前訂閱數(shù)據(jù)的變化,維度是數(shù)據(jù)本身,而不是變化的字段,可以考慮通過kotlin delegate 細化監(jiān)聽維度。

          加入我們

          歡迎加入字節(jié)跳動西瓜視頻客戶端團隊,我們專注于西瓜視頻 App 的開發(fā)和基礎(chǔ)技術(shù)建設(shè),在客戶端架構(gòu)、性能、穩(wěn)定性、編譯構(gòu)建、研發(fā)工具等方向都有投入。如果你也想一起攻克技術(shù)難題,迎接更大的技術(shù)挑戰(zhàn),歡迎加入我們 !

          西瓜視頻客戶端團隊正在熱招 Android、iOS 架構(gòu)師和研發(fā)工程師,最 Nice 的工作氛圍和成長機會,各種福利各種機遇,在北京、杭州、上海三地均有職位,歡迎投遞簡歷!

          聯(lián)系郵箱:[email protected];郵件標題:姓名+Android/iOS

          瀏覽 28
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  天天干天天操综合网 | www.翔田千里 | 日本丰满熟妇一国产成人免费一 | 国产精品激情综合 | 尻逼逼|