西瓜視頻 Android 端內數據狀態同步方案VM-Mapping

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"背景"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"西瓜在feed、詳情頁、個人主頁有一塊功能區,包括了點贊、收藏、關注等功能。這些功能長久以來都是孤立的:多個場景下點贊、收藏、關注等狀態或數量不一致。在以往的業務迭代中,都是業務A有了需求,就加個點讚的請求,把自己業務模塊的UI更新下就完事了,業務B也自己搞一下。當西瓜開始從切面發力互動業務的時候,這些問題就凸顯出來了。線上出現了很多在頁面A點贊\/收藏完一個視頻到頁面B點贊\/收藏狀態或者點贊\/收藏數不對的case。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/4d\/4d8733f4f5928540490b0e6285e676ad.jpeg","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"問題拆解"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在分析這塊業務時,梳理出幾種問題:"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"業務上場景太分散,體現到代碼上就是在activity、scene、viewholder、自定義view等各種個樣的容器,多個業務模塊、多個端(web、flutter)上都有很相似的操作,代碼跨度很大。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"存量的代碼中有些場景是處理過同步問題的,但是處理的又不徹底,方案也不一樣,比如有的情況用了全局註冊callback,來通知所有對結果敏感的場景;有的情況用了Eventbus;有的情況是更新內存,但是卻只是個別幾個模塊通用。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"一部分問題是原來的業務邏輯,比如,使用更新後的內存變量在多個頁面或者模塊傳遞引用,由於層次比較深引用值被中間的流程篡改。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"一部分問題是服務端數據邏輯問題。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中3、4點問題更像是邏輯bug。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"多個端的數據同步可以通過跨端事件,每個端收到事件後更新自己就行。所以最複雜最難搞的問題就是端內多場景下的數據狀態同步問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/c1\/c1117d743f61d77ff2621fa80065b6af.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"端內問題聚焦在幾個case:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"case1"},{"type":"text","text":":普通頁面,如Activity or Fragment上的狀態同步;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"case2"},{"type":"text","text":":feed卡片的狀態同步;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"case3"},{"type":"text","text":":feed卡片內多個複雜層級之間的狀態同步;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"case4"},{"type":"text","text":":以上的組合。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"目標"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"數據狀態同步,是要保證兩個一致性:數據一致性、UI一致性;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"方案要使用簡單,理解簡單;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"儘可能減少性能開銷。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"方案調研"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"EventBus"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個方案的本質是:監聽者收到事件->更新UI\/更新數據Model"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"對於case1"},{"type":"text","text":":如果是A頁面發起,B頁面被動接收,只需要在B頁面接收事件,更新B頁面的Model對象+UI即可。但是在收到事件之後,一定要把當前頁面的model對象更新,不然會有不一致的問題。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"對於case2"},{"type":"text","text":":"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"eventbus註冊在ViewHolder 上:由於ViewHolder的複用,ViewHolder的數量是少於“ListData”的,那麼意味着,只在ViewHolder上監聽,會出現那些沒有和ViewHolder 建立聯繫的數據無法被更新到。如果使用黏性事件,該事件會一直在內存中,粘性事件的膨脹不可控,很可能會造成嚴重的內存問題。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"eventbus註冊在Activity or 其它頁面上,收到事件後,遍歷數據列表,更新,然後通過RecyclerView的onDataItemChanged方法局部更新。但是在很多場景,比如西瓜feed,feed框架之下的view層次非常深。很多時候Rd只關注某類卡片下的某個UI組件,Feed框架和頂層頁面容器離的很遠,修改成本高,容易出錯,對feed框架或者頂層容器的侵入比較大。另外,onDataItemChanged的局部更新是ViewHolder 對應的itemView的,這個維度比較大,並不能刷新單獨的一個點贊按鈕。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"基於k-v的監聽、通知"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以對象id爲key,某個屬性值如點贊數爲value。事件發生時,將修改值寫入k-v列表,監聽者全部監聽這個變化。當新進入一個場景時,查詢k-v列表作爲最新值。這個方案和Eventbus粘性事件很像。"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"k-v 粒度太細,一直在內存中,非常容易膨脹,沒有合適的釋放時機,導致內存浪費;一旦移除,就可能概率的數據同步失效。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"k-v列表內的狀態要使用者在合適的時機同步到業務層數據Model。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"全局共享數據Model實例"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同一個數據Model對象,比如一個卡片Model,每次更新都是全局可見的。但是很明顯,"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對數據Model的要求很高。一個業務層數據Model類型,要全局統一,比如,一個視頻卡片業務層的類型是“ModelA”,那麼全局場景不能有“ModelB”表示卡片。在很多場景下,業務層會對原始數據Model進行包裝適配;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"內存佔用很大;可能要緩存很多個列表。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"基於註解的對象映射方案VM-Mapping"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"特點"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"以命名空間+指定字段值 爲key,匹配相同註解名的字段的映射,打平了Model類型的不同、層級嵌套的約束;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"直接更新結果到數據model(如article),與數據model視角的同步;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"打平了多個頁面、複雜view層級嵌套的差異;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"自動處理更新,使用者僅需要關心怎麼更新UI,不需要考慮數據Model的一致性;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"任意場景的支持。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"思考"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"數據狀態同步,到底同步的是什麼?"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"上述的方案中大致有幾個角色:事件、監聽者、數據Model、UI。到底誰應該是主導者?"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"基於事件的方案都需要把狀態同步給數據Model,能簡化嗎?"}]}]}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/b3\/b37b176936933210d84adff08df4c701.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個過程中有四個角色,三個操作。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"突破View層級的限制"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從MVVM說起。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"MVVM是一種軟件設計典範,用一種業務邏輯、數據、界面顯示分離的方法組織代碼。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/98\/985f9eab5dc6ecd03ab9bf73b26f8662.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"MVVM本質上是一種數據驅動UI的理念。從這個理念看,數據狀態同步,同步的是數據Model,UI的變更是由數據的變更引起的,真正關注的點應該在數據本身上。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/25\/253ae90bdca2fc4abc45ec1500ce4681.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣,就不再需要額外一個接受事件的“容器”,來控制數據和UI了。到現在,只有三個角色,兩個操作了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"再回過頭看,爲什麼跨頁面、跨多View層級很難找到一個通用方案,是因爲總在找一個“容器”來承載事件的接受,然後再做雙份(數據和View)的同步。而且這個“容器”通常本身就是一個頁面,或者其它不同層級上的view,本身就存在很多樣化,爲這種多樣化適配,就會讓事情變得複雜。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假如不再找額外的“容器”,直接把監聽綁定在數據上,那麼View層級的限制也就不存在了。因爲不管在什麼場景,什麼層級,真正的邏輯中心都是數據,View也是通過數據渲染出來的,View不關心自己在什麼層級,只關心數據的變化。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"突破類型的限制"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏有幾個類型的限制:"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"數據Model的類型是否只能一成不變,假如網絡請求的原始數據是A類型,在場景1直接用了A類型,在場景2爲了適配UI對A做了包裝:"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"class A{\n val diggStatus : Int\n}\n\nclass B {\n val a : A\n val showTipEnable : Boolean\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然類型不同,但是對A、B來說,都是要更新diggStatus的;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"2","normalizeStart":"2"},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"在Android,數據Model的類型是強類型,是從網絡由二進制流反序列化出來的,那麼同一個二進流,既可以反序列化成A類型,又可以反序列化成B類型,只要滿足反序列化規則就行。但是事實上,他們的業務本質還是一個東西。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"class A{\n val diggStatus : Int\n}\n\nclass B{\n val digg_status : Int\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"3","normalizeStart":"3"},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"事件本身也是一個數據,只是它是用戶操作發起的,表象看和數據Model無關,但是一個事件既然能更新某個數據Model,那他們一定存在着對應關係。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個問題的本質是,類型約束是語言特性,但是和業務屬性無關,只要他們能確認是一個業務含義,不管他們怎麼換“馬甲”,他們總是能匹配上的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣就演變成了:"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"怎麼確定兩個類型是一個業務含義;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"怎麼確定屬性的對應關係(字段匹配)。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一個好說,主要能有唯一的業務標識,就能確定是一個業務含義;怎麼確定屬性的對應關係呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現有的技術體系裏就有可以借鑑的思想:數據庫的使用。像jetpack 的Room組件:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"@Entity(tableName = \"users\")\ndata class User(\n @PrimaryKey(autoGenerate = true) var userId: Long,\n @ColumnInfo(name = \"user_name\")var userName: String,\n @ColumnInfo(defaultValue = \"china\") var address: String\n)\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到,我們只要要在應用層這麼定義一個數據Model叫User,爲它加上註解,就可以把數據庫中的字段和我們的數據對應上。那麼方案呼之欲出,註解是可以完成屬性匹配的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"於是乎整個流程就簡化成了:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/22\/222838cd40c2d34a87cb15ac9c882553.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個流程可以看到,只剩下了兩個角色,和兩個操作了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所謂數據更新UI,就是View-Model;數據映射數據,就是Data-Mapping,於是這個方案的名稱就是VM-Mapping。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"詳細設計"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"需要對上述抽象流程做實現。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"映射"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面說到,映射關係由註解維護,一個有三個註解:"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"Mappable註解 :"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"標註在class上,用來識別這個類是不是可以被處理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"annotation class Mappable(val mappingSpaces: Arrary)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中mappingSpace是命名空間,表示是“一類”數據,可以和數據庫表名對比理解,mappingSpace就是tableName。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"2","normalizeStart":"2"},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"PrimaryKey註解:"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"標記在字段上,被標記的字段作爲Model對象的唯一標識。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"mappingSpace+PrimaryKey的值,就是在映射關係中的唯一業務標識。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"@Target(AnnotationTarget.FIELD)\n@Retention(AnnotationRetention.RUNTIME)\nannotation class PrimaryKey"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"3","normalizeStart":"3"},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"MappableKey註解:"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"標註在字段上,需要被映射對應的字段"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"Target(AnnotationTarget.FIELD)\n@Retention(AnnotationRetention.RUNTIME)\nannotation class MappableKey(val value: String)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"映射關係說明:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/c4\/c47d8c6c59b4526de23b026d3f81defe.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"數據驅動UI"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Android裏有很多類似理念的東西,比如LiveData,就是數據更新通知到UI上。本質上數據驅動UI,就是在數據DataUI 之間建一個“橋樑”。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個不過LiveData並不適合用在這裏,理由是:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LiveData綁定的生命週期是LifecycleOwner,也就是Activity、Fragment維度,明顯我們的場景維度更細;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"直接observeForever也可以,但是由於View層級的多樣,調用方通常需要合適的時機移除;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LiveData 強引用了數據Data,這個“橋樑”本身對數據Data的生命週期造成了影響。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"VM-Mapping做了個簡單方案。用了兩級HashMap,一級HashMap使用業務唯一標識(mappingSpace+PrimaryKey的值)爲KEY,二級使用WeakHashMap,以數據Model實例爲KEY,XGViewModel爲VALUE。維護數據Data 和 UI回調之間的關係:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/de\/de6e7b4a92c6e3cbf44d444df3b1058d.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"XGViewModel維護了通知給UI的弱引用回調合集。一個數據Model實例對應了一個XGViewModel。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當映射發生時,會通過業務標識Key,查找所有還沒有被回收的數據Model實例,然後通過對應的XGViewModel通知UI自己的變更。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"總體流程"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/db\/dbbaef048cbcb5f9e60fecc2838bc6e4.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這個流程中,業務使用只需要關心發起映射數據和更新視圖。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲存在列表,那麼會有一個列表的維護者,就是所謂的映射中心。映射中心有兩個核心能力:"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"收集需要被更新的數據Model列表;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"查找匹配。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"其它細節"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲使用了反射,爲了減少性能損耗,會對收集的數據Model類型做class和相關字段的緩存。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"列表存在膨脹現象,二級弱引用列表的key是數據Model實例本身,當它被虛擬機回收的時候,會把一級列表中的該項移除,當一級列表某個key下沒有內容時,也會把該key移除。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"移除的時機在每次添加數據Model到列表;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"移除的條件是一級列表長度達到閾值。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是注意,這個移除並不會影響VM-Mapping的能力,因爲VM-Mapping關注的是數據本身,當數據被回收的時候,不會有任何場景會用到這個數據,自然也不用關心是不是需要通知到它。"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了避免影響主線程,和多線程競爭列表的問題,映射中心操作都在單子線程中處理。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"方案對比"}]},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"
方案優勢劣勢
Eventbus理解成本低事件、UI、數據Model三個角色都要保持一致,適配各種場景的成本高,不通用。
全局共享數據Model實例使用簡單條件苛刻;佔用內存,膨脹不可控制。
基於k-v的監聽、通知各場景通用粒度太細導致內存不可控制,移除策略會導致同步失效。事件需要手動同步數據Model。
VM-Mapping使用簡單,不需要手動同步回數據Model,在所有場景下通用。用到了反射,有一部分性能損耗。"}}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"方案收益"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"西瓜在之前遺留了大量的類似問題,一直沒有好的方案解決,要麼存在根本性缺陷,要麼實施成本高。VM-Mapping支持了在西瓜中視頻相關的核心場景快速接入,實現了線上點贊數異常問題清零。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"後續計劃"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據統計,由於使用運行時註解+反射,一個操作的耗時均值在10ms左右。仍然有可以優化的空間。可以考慮使用編譯時註解維護數據映射關係。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前訂閱數據的變化,維度是數據本身,而不是變化的字段,可以考慮通過kotlin delegate 細化監聽維度。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文轉載自:字節跳動技術團隊(ID:BytedanceTechBlog)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/rutSv-Rb1Y9xK2FPoSeeVw","title":"xxx","type":null},"content":[{"type":"text","text":"西瓜視頻 Android 端內數據狀態同步方案VM-Mapping"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章