<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重構(gòu)——圍繞于播放器的重構(gòu)實(shí)踐

          共 11644字,需瀏覽 24分鐘

           ·

          2021-10-24 02:06

          導(dǎo)讀:作為短視頻應(yīng)用最重要的組件,播放器扮演了至關(guān)重要的角色;對(duì)于短視頻App來(lái)說(shuō),播放相關(guān)的性能直接影響到核心用戶體驗(yàn),圍繞播放器做一系列的優(yōu)化是性價(jià)比極高的技術(shù)投資。

          全文3680字,預(yù)計(jì)閱讀時(shí)間12分鐘。

          一、背景介紹

          作為短視頻應(yīng)用最重要的組件,播放器扮演了至關(guān)重要的角色;對(duì)于短視頻App來(lái)說(shuō),播放相關(guān)的性能直接影響到核心用戶體驗(yàn),圍繞播放器做一系列的優(yōu)化是性價(jià)比極高的技術(shù)投資。經(jīng)過(guò)持續(xù)的重構(gòu)和優(yōu)化,好看視頻Android端的視頻播放體驗(yàn)基本達(dá)到秒開效果,瞬間起播,滑動(dòng)流暢度也明顯提高。我們先來(lái)看看重構(gòu)前后的對(duì)比。
          重構(gòu)前:很容易觀察視頻起播前有明顯的停頓,中低端機(jī)格外明顯。

          重構(gòu)后,幾乎感受不到播放切換的停頓感。
          本次就和大家分享一下好看視頻Android端重構(gòu)中的想法和心得,主要側(cè)重于架構(gòu)、性能優(yōu)化方面。

          二、好看視頻歷史回顧——單播放器

          好看視頻2016年起源于hao123的圖文信息流,歷經(jīng)4年,確立了信息流模式的卡尺播放模式。在2020年的Q3,好看視頻開啟了沉浸式全屏播放項(xiàng)目,最終推全。
          由于歷史原因,好看視頻一直是單播放器架構(gòu):全局一個(gè)播放器,飄在所有View的最上層,跟隨著左右滑動(dòng)的ViewPager和上下翻頁(yè)的RecyclerView(豎劃翻頁(yè)配合使用了PagerSnapHelper)同時(shí)移動(dòng)。
          mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
          @Override
          public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
          mVideoView.moveVideoViewByX(positionOffsetPixels);
          }
          }
          mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
          @Override
          public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
          mVideoView.moveVideoViewByY(dy);
          }
          });


          老架構(gòu)的核心特點(diǎn)是,播放器有極高的生命周期和作用域(和當(dāng)前Activity一致),且低作用域和生命周期的組件又直接持有Context控制和操作播放器。播放器全局單例雖然邏輯上簡(jiǎn)單,但實(shí)現(xiàn)上并不簡(jiǎn)約,主要存在幾個(gè)明顯問(wèn)題
          1、業(yè)務(wù)耦合嚴(yán)重,開發(fā)效率低
          • 播放器和業(yè)務(wù)代碼耦合嚴(yán)重,多個(gè)核心類代碼1萬(wàn)+行,維護(hù)成本高,對(duì)新人極其不友好。播放器在初始化時(shí)就有221個(gè)View,各個(gè)View之間的隱藏和顯示邏輯復(fù)雜,函數(shù)括號(hào)嵌套層次非常深,維護(hù)成本極高。Feed列表只承載視頻封面圖,導(dǎo)致廣告/直播等第三方業(yè)務(wù)既要負(fù)責(zé)holder的展示,又要獨(dú)立創(chuàng)建高層級(jí)的播放器進(jìn)行控制,代碼復(fù)雜度極高。
          • 播放器狀態(tài)控制復(fù)雜紊亂,從Activity、Fragment、ViewPager、RecyclerView、RecyclerViewAdapter、RecyclerViewHolder、每個(gè)View都能直接控制全局的單例播放器,生命周期難以追蹤,播放相關(guān)的bug和用戶反饋定位十分困難。
          2、性能問(wèn)題尾大不掉
          • 由于播放器是飄在所有View的最上層,導(dǎo)致某些業(yè)務(wù)的View如果需要在最頂層,只能放在播放器內(nèi)部再重新實(shí)現(xiàn)一遍。RecyclerViewHolder中的某些View,既要在holder中又要在播放器的View中,再加上歷史的陳舊代碼,線上大量出現(xiàn)播放器View初始化時(shí)的ANR和卡頓

          播放器本身的復(fù)雜度又使得性能的優(yōu)化難度和風(fēng)險(xiǎn)極大,再加上歷史原因,這就導(dǎo)致了老架構(gòu)feed滑動(dòng)性能很差,滑動(dòng)卡頓在中端機(jī)上十分明顯,和競(jìng)品差距巨大,以火焰圖為例:


          • Feed列表滑動(dòng)需要同步播放器進(jìn)行卡尺滑動(dòng)(包括播放器復(fù)位等),導(dǎo)致啟播速度人為劣化。
          • 低級(jí)別組件需要持有Activity級(jí)別的句柄,非常容易產(chǎn)生內(nèi)存泄漏。
          • 無(wú)法直接獲取Activity句柄的業(yè)務(wù),大量通過(guò)EventBus分發(fā)消息和控制邏輯,導(dǎo)致播放控制混亂(EventBus事件混亂和組件生命周期事件沖突等)。EventBus不僅加劇了內(nèi)存泄漏的風(fēng)險(xiǎn),還導(dǎo)致一些列的性能問(wèn)題。

          三、好看視頻重構(gòu)項(xiàng)目——多播放器

          不破不立
          我們最終決定圍繞播放器來(lái)對(duì)現(xiàn)有代碼進(jìn)行重構(gòu),將全局單例的播放器下沉到每個(gè)holder中,便于業(yè)務(wù)隔離和靈活調(diào)用播放器。在提高架構(gòu)合理性從而提升團(tuán)隊(duì)開發(fā)效率的同時(shí),順帶解決部分性能問(wèn)題:立項(xiàng)之時(shí),僅僅架構(gòu)優(yōu)化可能不足以說(shuō)服眾人,但性能的優(yōu)化帶來(lái)的基礎(chǔ)體驗(yàn)的優(yōu)化,不容小覷??赡軐?duì)于客戶端來(lái)說(shuō),往往是帶有性能優(yōu)化的重構(gòu),才是完美的重構(gòu)。


          新架構(gòu):多播放器實(shí)例,每個(gè)holder獨(dú)有播放器,播放器跟隨自己的holder滑動(dòng);播放器和業(yè)務(wù)解耦。
          新架構(gòu)的顯著特點(diǎn),就是降低了播放器作用域和生命周期:
          • 在holder內(nèi)實(shí)現(xiàn)播放器狀態(tài)自洽管理,直播/廣告等業(yè)務(wù)僅在holder就可以實(shí)現(xiàn)自身業(yè)務(wù)(包括播放控制等),減少無(wú)用邏輯,降低代碼耦合。
          • 通過(guò)LifecycleLite分發(fā)播放相關(guān)事件,降低對(duì)EventBus的依賴,降低組件間耦合和內(nèi)存泄漏風(fēng)險(xiǎn)。
          • 利用自定義PageSnapHelper等組件,集中優(yōu)化Feed列表啟播/預(yù)加載等核心播放體驗(yàn)。
          ”多快好省“,可以說(shuō)新架構(gòu)只實(shí)現(xiàn)了前三個(gè),但對(duì)于客戶端來(lái)說(shuō),刻意”節(jié)省內(nèi)存“不見得是好主意。一個(gè)播放器大約占用10M的虛擬內(nèi)存,大部分情況下App同時(shí)存在2-3個(gè)播放器實(shí)例,以20-30M的”空間“換取”時(shí)間“(開發(fā)節(jié)省的時(shí)間+性能提升的時(shí)間),看起來(lái)就是大賺小虧的買賣。


          不論是從火焰圖,還是從線上統(tǒng)計(jì)的掉幀率和業(yè)務(wù)卡頓、ANR來(lái)看,新架構(gòu)明顯具有更優(yōu)秀的滑動(dòng)性能和體驗(yàn)。而且即使目前依然耗時(shí)的部分,也比較容易修復(fù)和緩解。

          重構(gòu)前后的掉幀率對(duì)比:


          輕微掉幀次數(shù)/10分鐘

          嚴(yán)重掉幀次數(shù)/10分鐘

          重構(gòu)前

          350

          77

          重構(gòu)后

          150

          18

          關(guān)于起播時(shí)間的優(yōu)化

          視頻起播時(shí)間,對(duì)于短視頻App來(lái)說(shuō)至關(guān)重要,如果用戶要等待很久的緩沖才能看到視頻開始播放,離開App的概率就會(huì)增加。在老架構(gòu)下,單播放器實(shí)例會(huì)讓針對(duì)起播時(shí)間的專項(xiàng)優(yōu)化難以實(shí)現(xiàn),而新架構(gòu)則為此優(yōu)化提供了架構(gòu)層面的支持。
          1、關(guān)于播放器創(chuàng)建的時(shí)機(jī)
          根據(jù)RecyclerView的機(jī)制,當(dāng)前視頻在播放時(shí),下一個(gè)視頻的holder會(huì)提前調(diào)用onBindViewHolder準(zhǔn)備頁(yè)面和數(shù)據(jù),所以可以在RecyclerViewHolderonBind時(shí)就初始化下一個(gè)待播放視頻的播放器。
          @Override
          public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
          if (holder instanceof ImmersiveBaseHolder) {
          ((ImmersiveBaseHolder) holder).onBind(getData(position), position);
          holder.createPlayer();
          }
          }
          2、關(guān)于播放器開始播放(start)的時(shí)機(jī)
          一般情況下,我們會(huì)在RecyclerViewonScrollStateChanged中判斷列表滑動(dòng)的狀態(tài),當(dāng)RecyclerView滑動(dòng)停止時(shí)再起播,并結(jié)束上一個(gè)視頻的播放。
          mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
          @Override
          public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
          if (newState == SCROLL_STATE_SETTLING) {
          currentHolder.player.prepareAysnc();
          lastHolder.player.stopAndRelease();
          }
          }
          });
          這種方式固然可行,但我們能不能把播放器播放的時(shí)機(jī)再提前呢?
          當(dāng)我們將手指放在屏幕時(shí),View隨著手指滑動(dòng)而上下滑動(dòng);當(dāng)我們松開手指時(shí),PagerSnapHelper會(huì)計(jì)算要跳轉(zhuǎn)的視頻,并根據(jù)速度和剩余的滑動(dòng)距離計(jì)算時(shí)間,通過(guò)SmoothScroller做慣性滾動(dòng)動(dòng)畫——我們考慮下,如果在松開手指的一刻,換句話說(shuō),當(dāng)我們明確知道了下一個(gè)待播放的視頻時(shí),就趕緊播放它,會(huì)有什么效果?
          幾乎秒播
          從播放器調(diào)用prepareAsync到首幀渲染(onInfoMEDIA_INFO_VIDEO_RENDERING_START回調(diào)),大概需要300-500ms,而從手指開屏幕到滑動(dòng)結(jié)束,也接近200-300ms。一般來(lái)說(shuō),起播速度在200ms左右用戶幾乎可以認(rèn)為是”秒開“,所以提前起播對(duì)用戶體驗(yàn)的提升巨大。
          // PagerSnapHelper.java
          @Override
          protected LinearSmoothScroller createSnapScroller(RecyclerView.LayoutManager layoutManager) {
          return new LinearSmoothScroller(mRecyclerView.getContext()) {
          @Override
          protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
          int nextPosition = state.getTargetScrollPosition();
          adapter.getHolder(nextPosition).player.start();
          adapter.getHolder(currentPosition).player.stopAndRelease();
          }
          }
          }
          // 重點(diǎn)在 onTargetFound 此時(shí)已經(jīng)成功定位被選擇的holder
          甚至在性能一般的機(jī)器上,可以考慮更深層次的優(yōu)化,在onBindViewHolder中創(chuàng)建播放器后,立即prepare播放器,但不調(diào)用start。此類優(yōu)化需要對(duì)播放器的生命周期掌握極其熟練,處理不當(dāng)很容易導(dǎo)致多個(gè)視頻同時(shí)播放或者其他的隱藏bug,需要格外小心。
          3、更早的起播
          有同學(xué)可能會(huì)問(wèn),為什么不在holder剛出現(xiàn)在屏幕中的時(shí)候就起播呢?比如說(shuō)在holder的attachToWindow
          這樣會(huì)導(dǎo)致一個(gè)問(wèn)題,由于起播過(guò)早,當(dāng)屏幕停下來(lái)之后,視頻已經(jīng)播放了1-2s,對(duì)用戶來(lái)說(shuō)體驗(yàn)會(huì)很奇怪,永遠(yuǎn)看不到完整的短視頻,體驗(yàn)非但沒(méi)有變好,反而更差。
          但這個(gè)思路并不是毫無(wú)用處,他適用于一種特殊格式的視頻流:對(duì)于feed中出現(xiàn)有直播流的情況。提前起播直播流(假設(shè)是flv或者rtmp),但不播放直播的聲音,等到滑動(dòng)結(jié)束后再開始播放直播的聲音,效果就很贊。直播的特點(diǎn)在于,用戶不需要從第一幀看起,而且直播的起播往往比短視頻要慢,提前起播對(duì)于直播來(lái)說(shuō)是十分完美的解決方案,這個(gè)思路也是很多App的實(shí)現(xiàn)方式。

          關(guān)于新架構(gòu)的整體收益

          從開發(fā)效率上來(lái)說(shuō),普遍的主觀反映,提高了最少20%的開發(fā)效率,代碼更加好找,”歷史包袱“也少了很多。
          從技術(shù)指標(biāo)上來(lái)看,用戶感知的起播時(shí)間大幅提高了150ms,網(wǎng)絡(luò)不差的情況下基本能做到視頻秒開;而且后續(xù)的優(yōu)化也更簡(jiǎn)單,我們也在持續(xù)不斷的優(yōu)化每個(gè)細(xì)節(jié)。
          從業(yè)務(wù)指標(biāo)上看,留存率,人均播放視頻數(shù),使用時(shí)?等指標(biāo)均有不同程度的上漲,商業(yè)化收益也有所上漲,業(yè)務(wù)收益十分明顯。

          四、淺談播放器預(yù)加載

          所謂”預(yù)加載“,指的是提前下載好一定長(zhǎng)度的視頻,當(dāng)要播放該視頻的時(shí)候,播放器只需要下載一小部分,甚至可以直接立即起播。

          1、關(guān)于預(yù)加載的文件大小問(wèn)題

          加載太少,完全失去了秒開的效果;加載太多,對(duì)帶寬來(lái)說(shuō)又是一種浪費(fèi)(當(dāng)然,如果用戶十分活躍,滑動(dòng)意愿強(qiáng),可以預(yù)加載好完整的下一個(gè)視頻),一般來(lái)說(shuō),300K-500K是一個(gè)常見的選擇。
          $pip install qtfaststart
          $qtfaststart -l 曾經(jīng)的你.mp4

          ftyp (32 bytes)
          moov (6891 bytes)
          free (8 bytes)
          mdat (3244183 bytes)
          對(duì)于普通長(zhǎng)度的短視頻來(lái)說(shuō),300K可以包含幾幀的數(shù)據(jù),如上圖所示,文件頭占了不到100K,我們?cè)倏纯磶那闆r。
          $ffprobe 曾經(jīng)的你.mp4 -show_frames | grep -E 'pict_type|coded_picture_number|pkt_size'

          pkt_size=28604
          pict_type=I
          coded_picture_number=0
          pkt_size=145
          pkt_size=479
          pkt_size=568
          pict_type=B
          coded_picture_number=3
          pkt_size=476
          pkt_size=531
          pkt_size=1224
          pict_type=B
          coded_picture_number=2
          pkt_size=703
          視頻的第一幀是關(guān)鍵幀I幀,大約占了30K,B和P幀體積相對(duì)較小,所以不難估計(jì)300K可以渲染不少幀數(shù),基本滿足我們的需求;如果視頻較長(zhǎng)或者碼率較大,預(yù)加載長(zhǎng)度應(yīng)當(dāng)適度增加,最佳方案是后端轉(zhuǎn)碼端提前計(jì)算好數(shù)據(jù),端上根據(jù)這個(gè)建議值來(lái)加載對(duì)應(yīng)的大小。
          大多數(shù)播放器為了播放流暢度和音視頻同步的需要,本地都會(huì)有一個(gè)緩沖buffer,有的邏輯是按照幀數(shù)設(shè)置,比如說(shuō)20幀,也有的是根據(jù)時(shí)間來(lái)設(shè)置,比如1-2秒,并不一定300K就一定能起播,需要本地測(cè)試具體數(shù)值,必要時(shí)需要修改播放器內(nèi)核的配置。大部分情況下,好看的視頻在300K時(shí),自研的播放器能夠順利起播。

          2、關(guān)于預(yù)加載的時(shí)機(jī)問(wèn)題

          預(yù)加載時(shí)機(jī)如果太晚,幾乎沒(méi)有效果;如果太早,可能會(huì)跟當(dāng)前正在播放的視頻搶奪寶貴的帶寬資源,可能會(huì)導(dǎo)致起播速度收益遞減,甚至造成嚴(yán)重的卡頓。想象一下,如果我們快速滑動(dòng)ABC三個(gè)視頻,此時(shí)AB視頻正在預(yù)加載,正在播放的C視頻,C下面的D也在預(yù)加載,C的起播時(shí)間不但不會(huì)降低,反而還會(huì)提升!
          我們必須要遵守一個(gè)原則:視頻預(yù)加載絕對(duì)不能影響到當(dāng)前視頻的播放。一個(gè)簡(jiǎn)單的方案就是,當(dāng)前視頻緩沖到一定比例,再進(jìn)行下個(gè)視頻的預(yù)加載。當(dāng)然還有更加細(xì)致的方案,比如說(shuō)動(dòng)態(tài)計(jì)算當(dāng)前已緩沖的進(jìn)度(onBufferingUpdate回調(diào))和播放進(jìn)度(getCurrentPosition),如果當(dāng)前已經(jīng)緩沖好的時(shí)間足夠支撐到后續(xù)播放,就可以提前開啟下個(gè)視頻的預(yù)加載;當(dāng)前視頻如果已經(jīng)完播(onComplete回調(diào)),則可以放心大膽的對(duì)下個(gè)視頻加載更多長(zhǎng)度。
          另外,在視頻被滑走之后,播放器和預(yù)加載應(yīng)當(dāng)立即停止,釋放無(wú)用帶寬;否則,在網(wǎng)絡(luò)抖動(dòng)的情況下,預(yù)加載的競(jìng)爭(zhēng)會(huì)顯著加劇卡頓。
          好看視頻新架構(gòu)逐漸放量之后,播放卡頓率提高不少。我們經(jīng)過(guò)緊急排查,將懷疑的對(duì)象放到了視頻預(yù)加載策略上。重新梳理確定了預(yù)加載的細(xì)節(jié)后,卡頓率下降到之前水平,且用戶感知起播時(shí)間也沒(méi)有蛻化,收效很大。

          3、關(guān)于預(yù)加載庫(kù)AndroidVideoCache

          https://github.com/danikula/AndroidVideoCache
          這個(gè)庫(kù)近些年不再更新,bug很多,issue也不少,不適于在生產(chǎn)環(huán)境,但不失為學(xué)習(xí)預(yù)加載庫(kù)設(shè)計(jì)和實(shí)現(xiàn)的好資源,該庫(kù)的設(shè)計(jì)驗(yàn)證了一個(gè)計(jì)算機(jī)界的箴言:
          計(jì)算機(jī)科學(xué)領(lǐng)域的任何問(wèn)題都可以通過(guò)增加一個(gè)間接的中間層來(lái)解決。
          Any Problem in computer science can be sovled by another layer of indircetion.
          VideoCache作為播放器和遠(yuǎn)端資源(CDN)的中間層,一方面從遠(yuǎn)端緩存視頻到本地,一方面本機(jī)又開啟了一個(gè)server,響應(yīng)來(lái)自播放器的請(qǐng)求。


          但這個(gè)庫(kù)原本并不支持預(yù)加載固定數(shù)的字節(jié),只支持全部下載,我們可以先簡(jiǎn)單的實(shí)現(xiàn)一下部分預(yù)加載的功能。
          // in HttpProxyCacheServer.java
          static final int PRELOAD_CACHE_SIZE = 300 * 1024;

          public void preload(Context context, String url, int preloadSize) {
          socketProcessor.submit(new PreloadProcessorRunnable(url, preloadSize));
          }

          private final class PreloadProcessorRunnable implements Runnable {
          private final String url;
          private int preloadSize = PRELOAD_CACHE_SIZE;
          public PreloadProcessorRunnable(String url, int preloadSize) {
          this.url = url;
          this.preloadSize = preloadSize;
          }
          @Override
          public void run() {
          processPreload(url, preloadSize);
          }
          }
          private void processPreload(String url, int preloadSize) {
          try {
          HttpProxyCacheServerClients clients = getClients(url);
          clients.processPreload(preloadSize);
          clientsMap.remove(url);
          } catch (ProxyCacheException | IOException e) {
          e.printStackTrace();
          }
          }
          public void stopPreload(String url) {
          try {
          HttpProxyCacheServerClients clients = getClientsWithoutNew(url);
          if(clients != null) {
          clients.shutdown();
          }
          } catch (ProxyCacheException e) {
          e.printStackTrace();
          } catch (Exception e) {
          e.printStackTrace();
          }
          }
          // HttpProxyCacheServerClients.java
          public void processPreload(int preloadSize) throws ProxyCacheException, IOException {
          startProcessRequest();
          try {
          clientsCount.incrementAndGet();
          proxyCache.processPreload(preloadSize);
          } finally {
          finishProcessRequest();
          ProxyLogUtil.d(TAG, "processPreload finishProcessRequest");
          }
          }
          // HttpProxyCache.java
          public void processPreload(int preloadSize) throws IOException, ProxyCacheException {
          long cacheAvailable = cache.available();
          if (cacheAvailable < preloadSize) {
          byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
          int readBytes;
          long offset = cacheAvailable;
          while ((readBytes = read(buffer, offset, buffer.length)) != -1) {
          offset += readBytes;
          if (offset > preloadSize) break;
          }
          ProxyLogUtil.d(TAG, "preloaded url = " + source.getUrl() + ", offset = " + offset + ", preloadSize = " + preloadSize);
          }
          }
          // 僅供學(xué)習(xí)使用,不適用生產(chǎn)環(huán)境
          這樣一來(lái),我們就有了比較基礎(chǔ)的預(yù)加載(preload)和取消預(yù)加載(stopPreload)功能, 本身這個(gè)庫(kù)的實(shí)現(xiàn)并不復(fù)雜,有時(shí)間和人力完全可以自己開發(fā)一套適合自己業(yè)務(wù)的基礎(chǔ)庫(kù)。
          最近“端智能”概念很火,相比于預(yù)估點(diǎn)擊率(ctr)等嘗試增強(qiáng)推薦效果的各種想法,基于播放器的預(yù)加載的策略調(diào)優(yōu),可能是端智能最容易落地的方向。反證來(lái)看,前者如果有收益,那完全可以移植到推薦和算法端。后者理論上可以做到真正的“多快好省”:在顯著提高用戶感知的起播速度基礎(chǔ)上,保證不退化卡頓,還不會(huì)因?yàn)轭A(yù)加載增加太多的帶寬資源。

          五、淺談播放器卡頓

          對(duì)于播放場(chǎng)景較多的App來(lái)說(shuō),在ANR和卡頓數(shù)據(jù)里,播放器必定占據(jù)一席之地。以ijkplayer為例,release函數(shù)耗時(shí)明顯,甚至通過(guò)抓trace都能看到。

          播放器的創(chuàng)建和銷毀都是耗時(shí)的操作,很容易將主線程阻塞住,導(dǎo)致App卡頓甚至ANR。最直觀的解決方案就是將release函數(shù)放到子線程執(zhí)行,效果立竿見影;但如果是日活百萬(wàn)以上的應(yīng)用,會(huì)新增不少SIGABRT等native錯(cuò)誤,crash率提高很多。原因也很簡(jiǎn)單,同一個(gè)播放器,主線程和子線程同時(shí)操作player必然存在線程沖突問(wèn)題;某個(gè)線程已經(jīng)release了播放器,另外的線程還在不斷調(diào)用播放器的接口。
          1 long ijkmp_get_duration(IjkMediaPlayer *mp)
          2 {
          3 assert(mp);
          4 pthread_mutex_lock(&mp->mutex);
          5 long retval = ijkmp_get_duration_l(mp);
          6 pthread_mutex_unlock(&mp->mutex);
          7 return retval;
          8 }
          通過(guò)addr2line或者ndk-stack定位到有大量崩潰發(fā)生在第5行,mp為空指針導(dǎo)致crash。這個(gè)不難猜測(cè),既然App沒(méi)有crash在第3行的assert語(yǔ)句而崩潰在了后面,說(shuō)明必定發(fā)生了在這把鎖控制之外的線程問(wèn)題。一個(gè)簡(jiǎn)單的解決方案是再次加入判空處理,但此方案依然不能完全杜絕crash。
          static long ijkmp_get_duration_l(IjkMediaPlayer *mp)
          {
          if (mp == NULL) {
          return 0;
          }
          return ffp_get_duration_l(mp->ffplayer);
          }
          // NOTICE: 此方案仍存在線程沖突問(wèn)題
          想要完全解決,一個(gè)更優(yōu)雅的方案是:
          1、將同一個(gè)播放器的所有操作,包括創(chuàng)建和銷毀等都放在同一個(gè)子線程,建議放在一個(gè)HandlerThread中;
          2、業(yè)務(wù)側(cè)單獨(dú)增加變量isPlayerReleased,在播放器銷毀之前將此變量置為true,后面對(duì)播放器的所有操作都要直接忽略;
          需要注意的是,除了業(yè)務(wù)側(cè)主動(dòng)調(diào)用播放器之外,ijkplayer本身也有一個(gè)線程在操作內(nèi)核,播放器本身會(huì)周期性的主動(dòng)回調(diào)給業(yè)務(wù)。
          // in https://github.com/bilibili/ijkplayer/blob/master/android/ijkplayer/ijkplayer-java/src/main/java/tv/danmaku/ijk/media/player/IjkMediaPlayer.java

          private static class EventHandler extends Handler {
          @Override
          public void handleMessage(Message msg) {
          switch (msg.what) {
          case MEDIA_PREPARED:
          player.notifyOnPrepared();
          return;

          case MEDIA_PLAYBACK_COMPLETE:
          player.stayAwake(false);
          player.notifyOnCompletion();
          return;

          case MEDIA_BUFFERING_UPDATE:
          long bufferPosition = msg.arg1;
          if (bufferPosition < 0) {
          bufferPosition = 0;
          }
          ...
          此處也需要加入isPlayerReleased控制,在重新編譯播放器內(nèi)核之后,線上跟播放器相關(guān)的crash幾乎消失

          六、架構(gòu)、性能優(yōu)化的意義

          大規(guī)模的性能優(yōu)化,能顯著提高應(yīng)用的用戶體驗(yàn),進(jìn)一步會(huì)提高業(yè)務(wù)的核心指標(biāo),尤其對(duì)于用戶增長(zhǎng)和商業(yè)變現(xiàn)更有裨益。單純的技術(shù)性指標(biāo),比如冷啟動(dòng)速度和滑動(dòng)流暢度,并不能讓所有人信服,但如果我們能將之轉(zhuǎn)化為業(yè)務(wù)指標(biāo)的增益,認(rèn)可度就會(huì)大大提高。比如說(shuō),啟動(dòng)速度的優(yōu)化往往會(huì)帶來(lái)留存率的提升,流暢度的優(yōu)化可能會(huì)提高消費(fèi)類指標(biāo);既然技術(shù)服務(wù)于業(yè)務(wù),那技術(shù)很可能也能夠通過(guò)數(shù)據(jù)來(lái)證明自己的收益。如果我們負(fù)責(zé)的是電商App,可能會(huì)提高訂單轉(zhuǎn)化率千分之一;如果我們負(fù)責(zé)的是信息流資訊App,可能會(huì)提高人均觀看視頻個(gè)數(shù)、圖文feed閱讀量、使用時(shí)長(zhǎng),甚至還有可能因?yàn)檎宫F(xiàn)和消費(fèi)以及使用時(shí)長(zhǎng)上漲,直接提高商業(yè)化收入。
          所以我們不妨先把技術(shù)放在一旁,先把自己當(dāng)做產(chǎn)品經(jīng)理,以普通的業(yè)務(wù)迭代需求來(lái)替代技術(shù)優(yōu)化來(lái)思考。我們精心設(shè)計(jì)AB實(shí)驗(yàn),在代碼中主動(dòng)加入AB實(shí)驗(yàn)開關(guān),盯緊AB實(shí)驗(yàn)后臺(tái),隨時(shí)查看用戶反饋,帶著業(yè)務(wù)數(shù)據(jù)匯報(bào)收益。師出有名,我們就算不能證明用戶體驗(yàn)明顯變好,起碼也能證明沒(méi)有變得更差。一旦我們讓技術(shù)優(yōu)化的收益深入人心,即使是小型的優(yōu)化也立即有了存在的正當(dāng)理由。
          當(dāng)然,并不是所有的優(yōu)化都適合AB實(shí)驗(yàn),如果業(yè)務(wù)迭代十分頻繁,優(yōu)化又不能在短時(shí)間內(nèi)完成,等待我們的將是無(wú)窮和無(wú)情的代碼沖突。”大膽假設(shè),小心求證“,只要有了合適機(jī)會(huì),我們就應(yīng)該設(shè)計(jì)AB實(shí)驗(yàn)并時(shí)刻盯緊AB實(shí)驗(yàn)大盤看數(shù)據(jù),無(wú)論是對(duì)業(yè)務(wù)RD還是性能RD,或者架構(gòu)RD。


          技術(shù)交流,歡迎加我微信:ezglumes ,拉你入技術(shù)交流群。

          推薦閱讀:

          音視頻開發(fā)工作經(jīng)驗(yàn)分享 || 視頻版

          OpenGL ES 學(xué)習(xí)資源分享

          開通專輯 | 細(xì)數(shù)那些年寫過(guò)的技術(shù)文章專輯

          NDK 學(xué)習(xí)進(jìn)階免費(fèi)視頻來(lái)了

          推薦幾個(gè)堪稱教科書級(jí)別的 Android 音視頻入門項(xiàng)目

          覺(jué)得不錯(cuò),點(diǎn)個(gè)在看唄~

          瀏覽 67
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  亚洲无码高清在线视频 | 天天日y天天爽 | 黄色照片视频 | 操逼图首页 | 操比一区二区三区 |