項目中直播流每場直播由一張直播圖片作為展示入口,用于提示用戶此直播的概要。如下圖:
圖片
然而直播圖片和容器的寬高比例出現不一致的情況。針對此情況,采取背景圖 contentmode展示為 aspectFill 且高斯模糊,上層高清圖為 aspectfit,給用戶一種圖片填滿且能清除獲取信息的視覺體驗。如下圖:
圖片
然而服務端下發直播的圖片分辨率在1000 * 2000byte左右,占用內存大小為1000 * 2000 * 4,約為 2M 大小。資深直播用戶最多有一千場直播。使用 sd_webImage 下載圖片并緩存在內存中,查看足夠多的直播封面時,在iPhone 13機型,iOS15的手機,滑動到400場直播時,就會產生內存不足崩潰。且崩潰堆棧展示在進行高斯模糊的方法中。
經過初步分析,得出影響內存的原因有以下幾方面。
避免下載快速劃過的圖片
判斷用戶是否快速劃過,無需下載圖片。scrollViewDidScroll 的回調頻率是小于 CADisplayLink 回調頻率的,在滾動緩慢的狀態下,離散取整可能導致 contentOffset 在某次刷新中不發生變化,也就是說 didScroll 的兩次打點間隔有一定可能大于0.0167s,是2個或者3個刷新周期。低速狀態下本身差值的差別就不大,所以使用 didScroll 打點,默認間隔是0.0167s即可。在 scrollviewDidScroll 中記錄兩次 scrollview 移動的差值,經實驗證明,速速大于60pt,流視圖圖片視覺上呈模糊狀態,故以60pt作為暴力滑動的臨界點;
下一步,暴力滾動停止劃過圖片的下載。創建全局變量標記是否暴力滾動,在系統調用 cellForItem 方法時,判斷標記為是則不進行下載。非暴力滾動下,標記為否, cell 根據標記開啟下載當前的圖片;
以上適用于自然減速停止的滑動。然而暴力滾動之后用戶手動突然停止,此時標記雖已及時改為否,但展示在屏幕上的圖片都處于不下載的狀態。我們在 scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate: 代理表示停止拖拽,在此時機,將展示在屏幕上的 cell 重新下載圖片。具體代碼如下:
[self.collectionView.visibleCells enumerateObjectsUsingBlock:^(__kindof UICollectionViewCell * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { SVPGCLiveCollectionViewCell *cell = obj; if (!cell.yesToLoad) { cell.yesToLoad = YES; } }];
自定義 cell ,setYesToLoad方法中下載圖片,設置 yesToLoad為 Yes ,即可開啟下載當屏的圖片這種情況;
其他的滑動情況:滾動到流頂部,手動設置 contenOffset:animated:經測試,滾動的速度屬于暴力滾動。需要在 scrollViewDidScrollToTop 和 scrollViewDidEndScrollingAnimation 中設置當屏圖片重新下載;
內存存儲高斯模糊的容器為 NSCache ,NSCache 提供最大儲存值。根據直播 tab 業務需要,每屏展示的數量大約為15張左右。存儲的數值需比15張略大,保證頁面的流暢度。初設值為20,每張占用的內存在5M左右,20張存儲在100M,可接收范圍內;
高斯模糊由 vImage 方式修改為 GPUImage 方式,使用 CPU 處理圖像, GPUImage 在 GPU 上處理 filter ,經統計,在 iOS13 上,處理圖片濾鏡時間比快約2倍。GPU 處理 filter 圖片需要運行大量 openGL 代碼,GPUImage 封裝了 openGL,只需要調用接口就可以實現 filter 。GPUImage 沒有現成的合成一張底部高斯,上層高清的圖片濾鏡。GPUImage 支持自定義頂點著色器和片元著色器實現濾鏡。整體思路為,使用混合濾鏡將高斯濾鏡的紋理和原圖紋理按照坐標計算生成新的輸出。紋理鏈條如圖:
a. 初始化高斯濾鏡,并設置參數。
GPUImageGaussianBlurFilter *gaussionfilter = [[GPUImageGaussianBlurFilter alloc] init]; gaussionfilter.blurRadiusInPixels = 9;//數值越高,越糊。 [gaussionfilter forceProcessingAtSize:highDefiniImage.size];
說明:blurRadiusInPixels 決定高斯的卷積數。卷積數越高,模糊效果越明顯。forceProcessingAtSize:設置高斯過濾器的目標輸出分辨率
b. 初始始化原始圖?。GPUImage中靜態圖?的源對象為 GPUImagePicture類
GPUImagePicture *highImage = [[GPUImagePicture alloc] initWithImage:highDefiniImage];
通過傳?原圖的 image 對象?成 GPUImagePicture 。
c. 自定義頂點著色器和片元著色器創建自定義混合濾鏡。頂點著色器,是對頂點進行一系列操作的著色器,頂點除了有最基本的位置屬性,還包含其他屬性,比如紋理,法線等。通過頂點著色器,顯卡就知道頂點應該繪制在具體什么位置。針對每個頂點,頂點著色器都會執行一次。首先收到系統傳給他的數據(位置坐標),將數據處理成后續我們需要的數據。系統對頂點著色器輸出的頂點數據進行插值,并將插值結果傳遞給片段著色器。片段著色器根據插值結果計算最后屏幕上的像素顏色。 GPUImageTwoInputFilter 提供頂點著色器的代碼滿足需求,直接使用即可。頂點著色器的代碼如下所示:
NSString *const kGPUImageTwoInputTextureVertexShaderString = SHADER_STRING( attribute vec4 position; attribute vec4 inputTextureCoordinate; //attribute 標注屬性為輸入變量,inputTextureCoordinate為第一個輸入對象的坐標 attribute vec4 inputTextureCoordinate2; //inputTextureCoordinate2 為第二個輸入變量的坐標 varying vec2 textureCoordinate; // varying 標注屬性為在vertex shader和fragment shader之間傳遞數據,表示將第一個輸入對象的坐標傳遞給片段著色器 varying vec2 textureCoordinate2; //表示將第二個輸入對象的坐標傳遞給片段著色器 void main() { gl_Position = position; textureCoordinate = inputTextureCoordinate.xy; textureCoordinate2 = inputTextureCoordinate2.xy; });
GPUImage 包裝頂點著色器為 string,便于加載。每一行代碼的作用標注在注釋里。片元著色器,是接收頂點著色器傳過來的數據,進行像素顏色計算。首先,需要傳入畫布和原圖的大小,用于計算畫布從原圖采點的坐標。其次,計算變量,我們需要確定上層高清圖在畫布上的leftX 和 rightX ,topY 和 bottomY,高度 targetH,寬度 targetW 。以及底部高斯背景圖片設置為 aspectFill 的高度和寬度。以上數值的計算涉及高清圖橫圖和豎圖。見下圖,左圖為豎圖,右圖為橫圖:
圖片
相應代碼如下:
if (drawableW/drawableH > imageW/imageH ){//豎版 targetW = imageW * (drawableH/imageH); lowp float left = (drawableW - targetW)/float(2); leftX = left/drawableW; rightX = (left+targetW)/drawableW; targetH = drawableH; bottomY = 1.0; targetHFill = imageHFill * drawableW / imageWFill; targetWFill = drawableW; }else{//橫版 targetH = imageH * (drawableW/imageW); lowp float top = (drawableH - targetH)/float(2); topY = top/drawableH; bottomY = (top+targetH)/drawableH; targetW = drawableW; rightX = 1.0; targetWFill = imageWFill * drawableH / imageHFill; targetHFill = drawableH; }
片元著色器通過 gl_FragColor = texture2D ( 參數1:輸?對象的紋理,參數2:輸入對象的坐標) 得到當前坐標的紋理。參數1由直接取值,參數2需要計算。坐標的計算分2種情況,?清圖和?斯背景,當畫布坐標處于 leftX 和 rightX , topY 和 bottomY 之間,則繪制?清圖,應取?清圖的相應坐標。相應坐標的計算為繪制的 點從畫布坐標換算到在?清圖上的坐標(坐標都為0-1區間值)如圖:
圖片
目標為計算出高清圖的0.2,0.1。換算代碼如下:
if (textureCoordinate2.x >= leftX && textureCoordinate2.x <= rightX && textureCoordinate2.y >= topY && textureCoordinate2.y <= bottomY) {lowp float offsetX = textureCoordinate2.x - leftX; //offsetx為圖中X的間距在畫布的占比。lowp float x = (offsetX * drawableW)/targetW; //X的距離在深灰高清圖中距離。lowp float offsetY = textureCoordinate2.y - topY;lowp float y = (offsetY * drawableH)/targetH;}
由公式 offX* drawableW = x *targetW 左右兩邊都是 X 線段的實際距離,從而得出 x 的值。同理得出 Y 的值。當畫布的坐標落在上圖淺灰色區域,采點高斯。高斯展示方式為 aspectFill. 如下圖:
圖片
粉色部分為高斯圖片的布局。高寬都會溢出被剪切一部分。圖中三角形為畫布中要繪制的點,此點在高斯圖中的位置為 targetY/targetHfill。targetY = (textureCoordinate2.y * drawableH + (targetHFill - drawableH )/float(2)) 為高斯圖被剪裁的上高度+畫布繪制點據畫布頂部的距離。具體代碼如下:
lowp float y = (textureCoordinate2.y * drawableH + (targetHFill - drawableH )/float(2))/ targetHFill;lowp float x = (textureCoordinate2.x * drawableW + (targetWFill - drawableW )/float(2))/ targetWFill;gl_FragColor = texture2D(inputImageTexture, vec2(x, y));
x 的計算方式和 y 值相同。到此,片元著色器的工作完成。
d. GPUImage加載著色器,并返回 UIImage 對象。自此,一張高斯背景疊加高清的圖片生成了。在直播流中,不止有一張需要合成,且合成是較耗時操作,需將合成操作放在子線程異步執行。執行完成后同步回到主線程展示。
e. 在使用 GPUImage 時,有?個需要注意的地?。GPUImage 底層使?的 是 openGL , openGL 在后臺進行渲染會導致 app 崩潰。所以需要我們退出后臺時,停止 openGl 渲染。采取的方式有三種,第?種,將合成隊列在退出后臺前設 置 suspend = yes ,還未開始的合成任務將不再執行,直至進入前臺后設 置 suspend = no 。然而,這種方式不能規避還在進行中的任務。于是第?種,?定 義 NSOperation ,執? operation 時。系統會調用 NSOperation 的 main 函數, 在 main 函數中,寫實現代碼。在每?句實現代碼前都判斷是否 cancled 。如果 cancled ,直接 return 。在系統即將進?后臺時,將在運行的任務 cancle 掉, operation 繼續執行,監測到已經 cancle 了,就會 return 。然而,粒度還是不夠細。如果任務執行到最后一行,且最后一行有 openGL 操作,那么就會攔截不到。第三種, GPUImage 的渲染都是放在??的?個隊列同步執?,在接收到系統將進入后臺的通知中,加入渲染隊列?個同步空任務,則系統會執行完空任務前的所有 任務之后,再進入后臺。從而避免后臺渲染。
自此,多張合成圖片的方案就結束了,結合線上數據,本頁內存消耗節約10M左右,每張合成圖片時間節約8ms左右。
本文鏈接:http://www.tebozhan.com/showinfo-26-13291-0.html直播流??內存優化?案
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 微軟將在 Windows 中棄用 VBScript
下一篇: Minium-小程序自動化測試框架