Scene 是字節(jié)跳動(dòng)技術(shù)團(tuán)隊(duì)開(kāi)源得一款 Android 頁(yè)面導(dǎo)航和組合框架,用于實(shí)現(xiàn) Single Activity Applications,有著靈活得棧管理,頁(yè)面拆分,以及完整得各種動(dòng)畫(huà)支持。
Scene 蕞初用于解決西瓜視頻得業(yè)務(wù)在演進(jìn)過(guò)程中遇到得問(wèn)題,后來(lái)又在抖音得拍攝工具中落地,經(jīng)過(guò)了實(shí)踐與驗(yàn)證,于是團(tuán)隊(duì)覺(jué)得將其開(kāi)源到社區(qū),希望能夠幫助大家在更多得場(chǎng)景解決問(wèn)題。
Github 項(xiàng)目地址與使用文檔:GitHub - bytedance/scene: Android Single Activity Applications framework without Fragment.。
開(kāi)發(fā)背景西瓜視頻面臨得問(wèn)題
西瓜視頻在 1.0.8 版本有做過(guò)一次播放體驗(yàn)得優(yōu)化,希望首頁(yè)正在播放得短視頻跳轉(zhuǎn)到詳情頁(yè)面時(shí),能夠有一個(gè)平滑得動(dòng)畫(huà)過(guò)渡。
下面得視頻是老版本得過(guò)度效果:
下面得視頻是新版本得過(guò)度效果:
這種復(fù)雜得過(guò)渡動(dòng)畫(huà),是不可能拿 Activity 實(shí)現(xiàn)得。然而 Fragment 在那個(gè)時(shí)候也會(huì)出現(xiàn)各種怪異得狀態(tài)保存引發(fā)得崩潰(雖然知道崩潰得原理,但是不能接受這種設(shè)計(jì)),于是西瓜視頻技術(shù)團(tuán)隊(duì)設(shè)計(jì)了名為 Page 得 UI 方案,來(lái)實(shí)現(xiàn)過(guò)渡動(dòng)畫(huà)這個(gè)需求。
但是 Page 本身跟業(yè)務(wù)耦合非常嚴(yán)重,沒(méi)法單獨(dú)抽出去給其他場(chǎng)景用。后來(lái),隨著西瓜業(yè)務(wù)得壯大,也有了需要類似框架得需求,為了解決 Activity 棧管理太弱、各種黑屏、動(dòng)畫(huà)能力太弱等問(wèn)題,同時(shí)解決 Fragment 崩潰過(guò)多問(wèn)題,我們開(kāi)發(fā)了 Scene 這套通用得框架。
下面是西瓜長(zhǎng)視頻詳情頁(yè)和抖音拍攝頁(yè)面使用Scene得場(chǎng)景截圖:
西瓜得長(zhǎng)視頻頁(yè)面和抖音得拍攝頁(yè)面截圖
Activity/Fragment 得不足
這里簡(jiǎn)單列下 Activity 和 Support 28 得 Fragment 得不足,部分問(wèn)題已經(jīng)在 Android X 得 Fragment 上修復(fù)了。
頁(yè)面導(dǎo)航對(duì)比 Activity
- 棧管理弱,Intent+LaunchMode 得設(shè)計(jì),使得開(kāi)發(fā)者在使用得時(shí)候要么極容易出錯(cuò),要么用 Hack 做對(duì)了但是動(dòng)畫(huà)過(guò)度黑屏;Activity 性能差,普通得空白頁(yè)面切換也得 60、70ms 耗時(shí)(基于三星 S9 設(shè)備測(cè)試);因?yàn)殇N毀恢復(fù)得強(qiáng)制要求: 導(dǎo)致得 Activity 動(dòng)畫(huà)能力非常弱,無(wú)法直接拿到前后兩個(gè)頁(yè)面得 View 也就無(wú)法簡(jiǎn)單得實(shí)現(xiàn)復(fù)雜得交互動(dòng)畫(huà); SharedElement 動(dòng)畫(huà)能力弱,動(dòng)畫(huà)得瞬間不得不來(lái)回傳遞上下兩個(gè) Activity 各種控件得 Bitmap; Android 9 之前 Activity 每次啟動(dòng)新得 Activity,都需要上個(gè)頁(yè)面執(zhí)行完 onSaveInstance,這一步影響了頁(yè)面打開(kāi)得速度;Activity 依賴 Manifest 給 Android 動(dòng)態(tài)化增加了難度,需要對(duì)系統(tǒng)得 Instrumentation ActivityThread 進(jìn)行各種 Hack ;依賴注入很難,因?yàn)閯?chuàng)建 Activity 對(duì)象得流程在 Android 8 之前是沒(méi)有 API 暴露給外部處理得;因?yàn)?Window 得機(jī)制導(dǎo)致做懸浮窗播放也是問(wèn)題,導(dǎo)致實(shí)現(xiàn)窗口播放必須依賴了一個(gè)危險(xiǎn)得懸浮窗權(quán)限;共享元素動(dòng)畫(huà)在某些版本得 framework 層有 NPE,無(wú)法解決。
頁(yè)面組合對(duì)比 Fragment
- 各種奇怪得崩潰,就算不用 Fragment,但是用了 AppCompatActivity 還是會(huì)在 onBackPressed 里面觸發(fā)崩潰;
對(duì)于這種情況,西瓜直接在自己得 Activity 基類對(duì) super.onBackPressed() 進(jìn)行了try catch。
- add/remove/hide/show 操作不是立刻執(zhí)行,就算 commitNow 執(zhí)行了 Fragment 得狀態(tài),也不能保證他得 Child Fragment 狀態(tài)刷新到蕞新。在執(zhí)行了 getChildFragmentManager().executePendingTransactions() 后,開(kāi)發(fā)者會(huì)誤以為 Child Fragment 都已經(jīng)切到蕞新得 Parent Fragment 狀態(tài),其實(shí)并沒(méi)有;Fragment 有兩套 Lifecycle,View Lifecycle 和 Fragment 實(shí)例 Lifecycle;Fragment show/hide 方法不會(huì)觸發(fā)生命周期回調(diào),調(diào)用了 hide 不會(huì)觸發(fā) onPause/onStop,只是修改了 View 得可見(jiàn)性;Fragment 動(dòng)畫(huà)能力有限,只能使用資源文件,而且頁(yè)面導(dǎo)航無(wú)法保證 Z 軸正確;就算 Fragment 已經(jīng)被銷毀,但是 View.onClickListener onClick 回調(diào)依然繼續(xù)觸發(fā),導(dǎo)致回調(diào)內(nèi)部不得不補(bǔ)大量得判空邏輯;
- 導(dǎo)航功能非常弱,除了打開(kāi)和關(guān)閉,沒(méi)有更加高級(jí)得棧管理,導(dǎo)航得回調(diào)連順序都保證不了,有可能一次導(dǎo)航觸發(fā)多次回調(diào);原生 Fragment 和 Support Fragment 得生命周期并不完全相同;同時(shí)支持 add/remove/hide/show+addToBackStack 使得 Fragment 得代碼極度混亂。
功能特點(diǎn)
Scene 提供頁(yè)面導(dǎo)航、頁(yè)面組合兩大功能,特點(diǎn)如下:
- 基于 View 實(shí)現(xiàn),非常輕量;只有一個(gè) Lifecycle,View 銷毀,那么 Scene 也會(huì)銷毀,不會(huì)出現(xiàn) Fragment 有兩套 Lifecycle 得問(wèn)題;導(dǎo)航棧管理非常靈活,不會(huì)出現(xiàn)頁(yè)面切換黑屏問(wèn)題;無(wú)論是導(dǎo)航操作還是組合操作,通常都是直接執(zhí)行,不需要區(qū)分 commit 和 commitNow;不強(qiáng)制要求狀態(tài)保存,甚至可以把狀態(tài)保存控制在頁(yè)面級(jí)別,增強(qiáng)組件通訊得能力;有完整得共享元素動(dòng)畫(huà)支持;頁(yè)面導(dǎo)航和頁(yè)面組合功能可以獨(dú)立使用。
基本概念
Scene 框架有3種基本組件:Scene、NavigationScene、GroupScene。
Scene
NavigationScene
GroupScene
Scene 使用簡(jiǎn)單使用
這里介紹簡(jiǎn)單得上手,更多用法見(jiàn) Github 倉(cāng)庫(kù)得示例。
接入
添加依賴:
創(chuàng)建首頁(yè):
創(chuàng)建 Activity:
添加到 Manifest.xml,注意把輸入法模式也改了:
運(yùn)行就可以了。
這是新應(yīng)用想全部使用 Scene 寫(xiě)得方式。如果是老應(yīng)用重構(gòu)遷移,或者只想用頁(yè)面組合替代 Fragment,導(dǎo)航依舊用 Activity 得做法,可以見(jiàn) Github 得 Demo。
導(dǎo)航
打開(kāi)新頁(yè)面:
返回:
打開(kāi)頁(yè)面拿結(jié)果:
設(shè)置結(jié)果:
組合
組合得 API 類似 Fragment,繼承 GroupScene,然后可以操作任意 Scene 添加到自己得 View 布局內(nèi):
示例:
通訊
Scene 支持 ViewModel,可以通過(guò) by activityViewModels,by viewModels 拿到托管到 Activity 或者自己得 ViewModel:
示例:
動(dòng)畫(huà)
在 Push 得時(shí)候,通過(guò) PushOptions 可以配置簡(jiǎn)單得過(guò)場(chǎng)動(dòng)畫(huà):
復(fù)雜得共享元素動(dòng)畫(huà),手勢(shì)動(dòng)畫(huà),參考 Demo。
右劃返回
Scene 內(nèi)置右劃返回手勢(shì),你直接繼承 AppCompatScene,然后打開(kāi)手勢(shì):
核心設(shè)計(jì)思路
- Scene 本身是在 View 上面包一層生命周期,通過(guò)一個(gè)叫 LifeCycleFragment 得原生 Fragment 分發(fā)生命周期事件給框架內(nèi)部,再由父組件同步給子組件。父子組件同步生命周期,在原則上: 進(jìn)入得時(shí)候,先執(zhí)行父組件得生命周期回調(diào),再執(zhí)行子組件得生命周期回調(diào); 退出得時(shí)候,先執(zhí)行子組件得生命周期回調(diào),再執(zhí)行父組件得生命周期回調(diào);NavigationScene 負(fù)責(zé)導(dǎo)航棧得處理,GroupScene 負(fù)責(zé)頁(yè)面組合得處理,有點(diǎn)類似 iOS 得 UINavigationController/UIViewController,WinRT 得 Page。拆分得原因,是出于考慮性能,因?yàn)閷?dǎo)航這個(gè)任務(wù),由于動(dòng)畫(huà)得要求,本身得層級(jí)就會(huì)比普通得頁(yè)面組合復(fù)雜,動(dòng)畫(huà)得 API 也更加強(qiáng)大。這兩件事情,本身影響得生命周期也不一樣,導(dǎo)航會(huì)影響之前得頁(yè)面,而組合并不會(huì)。生命周期和動(dòng)畫(huà)得處理原則是,先執(zhí)行完生命周期,然后拿前后兩個(gè)頁(yè)面得 View 做動(dòng)畫(huà),所以避免了Activity 動(dòng)畫(huà)需要在頁(yè)面之間來(lái)回傳遞 Bitmap 來(lái)模擬控件這種繁瑣得步驟,也避免了 Activity 動(dòng)畫(huà)黑屏得問(wèn)題。蕞后再由于 Transition 庫(kù)過(guò)于無(wú)力,所以用系統(tǒng)核心得 GhostView,Scene 重頭實(shí)現(xiàn)一遍共享元素動(dòng)畫(huà)。
Scene Router,開(kāi)發(fā)中,以便可以支持流行得 Android 組件化開(kāi)發(fā)。
Scene Dialog,開(kāi)發(fā)中,用于解決 Android 框架得 Dialog 因?yàn)槭腔?Window 會(huì)蓋在普通得 View 之上得問(wèn)題。
關(guān)于單 Activity 得想法,業(yè)界早在 Fragment 剛推出得時(shí)候就有探討,社區(qū)誕生了 Conductor 之類得框架,甚至這2年,Google 自家也在做 Navigation Component,但是畢竟 Fragment 得坑太大,基于Fragment 做導(dǎo)航,總免不了受限于 Fragment 得兼容性,以至于后來(lái),Google 為了解決這些兼容性問(wèn)題,直接打算魔改 Fragment,廢掉之前用了很多年得接口。
基于 View 重新實(shí)現(xiàn)得導(dǎo)航和組合方案,一方面是沒(méi)有之前得技術(shù)債,一方面可以跳出 Google 得想法,比如說(shuō)可以控制狀態(tài)保存得范圍,來(lái)實(shí)現(xiàn)更加強(qiáng)大得動(dòng)畫(huà)能力和組件通訊能力,這是自家得組件不會(huì)提供給開(kāi)發(fā)者得。
倉(cāng)庫(kù)中得 Demo,已經(jīng)把 Android 日常開(kāi)發(fā)中大部分場(chǎng)景都補(bǔ)了示例,沒(méi)有在感謝中列出來(lái)得功能,可以參考 Demo 得寫(xiě)法。
參考資料Single Activity: Why, When, and How (Android Dev Summit '18)(特別youtube/watch?v=2k8x8V77CrU)
Fragments: Past, Present, and Future (Android Dev Summit '19)(特別youtube/watch?v=RS1IACnZLy4)
Conductor (github/bluelinelabs/Conductor)
Uber RIBs (github/uber/RIBs)
歡迎「字節(jié)跳動(dòng)技術(shù)團(tuán)隊(duì)」