直播流??內(nèi)存優(yōu)化?案
01項目背景介紹
項目中直播流每場直播由一張直播圖片作為展示入口,用于提示用戶此直播的概要。如下圖:
圖片
然而直播圖片和容器的寬高比例出現(xiàn)不一致的情況。針對此情況,采取背景圖 contentmode展示為 aspectFill 且高斯模糊,上層高清圖為 aspectfit,給用戶一種圖片填滿且能清除獲取信息的視覺體驗。如下圖:
圖片
然而服務端下發(fā)直播的圖片分辨率在1000 * 2000byte左右,占用內(nèi)存大小為1000 * 2000 * 4,約為 2M 大小。資深直播用戶最多有一千場直播。使用 sd_webImage 下載圖片并緩存在內(nèi)存中,查看足夠多的直播封面時,在iPhone 13機型,iOS15的手機,滑動到400場直播時,就會產(chǎn)生內(nèi)存不足崩潰。且崩潰堆棧展示在進行高斯模糊的方法中。
02分析問題
經(jīng)過初步分析,得出影響內(nèi)存的原因有以下幾方面。
- 圖片分辨率高,高斯模糊占用的內(nèi)存越高。因為需要對進行大量模糊計算;
- 用戶快速滑動直播流,正常的下載圖片速度會展示所有劃過的圖片,高斯模糊在圖片下載完成 block 中執(zhí)行,即使劃過的直播圖,也會繼續(xù)高斯模糊直至返回。這樣會導致用戶大量無意義圖片占用大量內(nèi)存;
- 兩張 ImageView 需要在內(nèi)存中加載兩張一樣的圖片,是一種內(nèi)存浪費;
- 為了用戶查看圖片的及時性和流暢性,項目中沒有設置存儲高斯模糊圖片最大占用內(nèi)存。這會導致圖片內(nèi)存只會在內(nèi)存警告時被清除。導致高斯模糊不能獲取足夠的內(nèi)存而崩潰;
- 高斯模糊采用 vImage 方案,占用 CPU 進行高斯模糊計算,CPU 繁忙不能及時釋放內(nèi)存,進一步加劇內(nèi)存緊張。
03針對問題,采取措施
降低圖片分辨率
- 通?降低分辨率?式為采用圖片云開發(fā)系統(tǒng)提供的服務。在圖片 url 中加入分辨率的參數(shù), 直接下載相應分辨率的圖?。分辨率的設置,以圖片清晰為標準,?般設置為展 示 ImageView 的大小。這樣不消耗客戶端的資源,不會給 CPU 帶來額外的工作。
判斷用戶是否快速劃過,無需下載圖片。scrollViewDidScroll 的回調(diào)頻率是小于 CADisplayLink 回調(diào)頻率的,在滾動緩慢的狀態(tài)下,離散取整可能導致 contentOffset 在某次刷新中不發(fā)生變化,也就是說 didScroll 的兩次打點間隔有一定可能大于0.0167s,是2個或者3個刷新周期。低速狀態(tài)下本身差值的差別就不大,所以使用 didScroll 打點,默認間隔是0.0167s即可。在 scrollviewDidScroll 中記錄兩次 scrollview 移動的差值,經(jīng)實驗證明,速速大于60pt,流視圖圖片視覺上呈模糊狀態(tài),故以60pt作為暴力滑動的臨界點;
下一步,暴力滾動停止劃過圖片的下載。創(chuàng)建全局變量標記是否暴力滾動,在系統(tǒng)調(diào)用 cellForItem 方法時,判斷標記為是則不進行下載。非暴力滾動下,標記為否, cell 根據(jù)標記開啟下載當前的圖片;
以上適用于自然減速停止的滑動。然而暴力滾動之后用戶手動突然停止,此時標記雖已及時改為否,但展示在屏幕上的圖片都處于不下載的狀態(tài)。我們在 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:經(jīng)測試,滾動的速度屬于暴力滾動。需要在 scrollViewDidScrollToTop 和 scrollViewDidEndScrollingAnimation 中設置當屏圖片重新下載;
內(nèi)存存儲高斯模糊的容器為 NSCache ,NSCache 提供最大儲存值。根據(jù)直播 tab 業(yè)務需要,每屏展示的數(shù)量大約為15張左右。存儲的數(shù)值需比15張略大,保證頁面的流暢度。初設值為20,每張占用的內(nèi)存在5M左右,20張存儲在100M,可接收范圍內(nèi);
高斯模糊由 vImage 方式修改為 GPUImage 方式,使用 CPU 處理圖像, GPUImage 在 GPU 上處理 filter ,經(jīng)統(tǒng)計,在 iOS13 上,處理圖片濾鏡時間比快約2倍。GPU 處理 filter 圖片需要運行大量 openGL 代碼,GPUImage 封裝了 openGL,只需要調(diào)用接口就可以實現(xiàn) filter 。GPUImage 沒有現(xiàn)成的合成一張底部高斯,上層高清的圖片濾鏡。GPUImage 支持自定義頂點著色器和片元著色器實現(xiàn)濾鏡。整體思路為,使用混合濾鏡將高斯濾鏡的紋理和原圖紋理按照坐標計算生成新的輸出。紋理鏈條如圖:
a. 初始化高斯濾鏡,并設置參數(shù)。
GPUImageGaussianBlurFilter *gaussionfilter = [[GPUImageGaussianBlurFilter alloc] init];
gaussionfilter.blurRadiusInPixels = 9;//數(shù)值越高,越糊。
[gaussionfilter forceProcessingAtSize:highDefiniImage.size];
說明:blurRadiusInPixels 決定高斯的卷積數(shù)。卷積數(shù)越高,模糊效果越明顯。forceProcessingAtSize:設置高斯過濾器的目標輸出分辨率
b. 初始始化原始圖?。GPUImage中靜態(tài)圖?的源對象為 GPUImagePicture類
GPUImagePicture *highImage = [[GPUImagePicture alloc] initWithImage:highDefiniImage];
通過傳?原圖的 image 對象?成 GPUImagePicture 。
c. 自定義頂點著色器和片元著色器創(chuàng)建自定義混合濾鏡。頂點著色器,是對頂點進行一系列操作的著色器,頂點除了有最基本的位置屬性,還包含其他屬性,比如紋理,法線等。通過頂點著色器,顯卡就知道頂點應該繪制在具體什么位置。針對每個頂點,頂點著色器都會執(zhí)行一次。首先收到系統(tǒng)傳給他的數(shù)據(jù)(位置坐標),將數(shù)據(jù)處理成后續(xù)我們需要的數(shù)據(jù)。系統(tǒng)對頂點著色器輸出的頂點數(shù)據(jù)進行插值,并將插值結(jié)果傳遞給片段著色器。片段著色器根據(jù)插值結(jié)果計算最后屏幕上的像素顏色。 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之間傳遞數(shù)據(jù),表示將第一個輸入對象的坐標傳遞給片段著色器
varying vec2 textureCoordinate2; //表示將第二個輸入對象的坐標傳遞給片段著色器
void main()
{
gl_Position = position;
textureCoordinate = inputTextureCoordinate.xy;
textureCoordinate2 = inputTextureCoordinate2.xy;
}
);
GPUImage 包裝頂點著色器為 string,便于加載。每一行代碼的作用標注在注釋里。片元著色器,是接收頂點著色器傳過來的數(shù)據(jù),進行像素顏色計算。首先,需要傳入畫布和原圖的大小,用于計算畫布從原圖采點的坐標。其次,計算變量,我們需要確定上層高清圖在畫布上的leftX 和 rightX ,topY 和 bottomY,高度 targetH,寬度 targetW 。以及底部高斯背景圖片設置為 aspectFill 的高度和寬度。以上數(shù)值的計算涉及高清圖橫圖和豎圖。見下圖,左圖為豎圖,右圖為橫圖:
圖片
相應代碼如下:
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 ( 參數(shù)1:輸?對象的紋理,參數(shù)2:輸入對象的坐標) 得到當前坐標的紋理。參數(shù)1由直接取值,參數(shù)2需要計算。坐標的計算分2種情況,?清圖和?斯背景,當畫布坐標處于 leftX 和 rightX , topY 和 bottomY 之間,則繪制?清圖,應取?清圖的相應坐標。相應坐標的計算為繪制的 點從畫布坐標換算到在?清圖上的坐標(坐標都為0-1區(qū)間值)如圖:
圖片
目標為計算出高清圖的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 的值。當畫布的坐標落在上圖淺灰色區(qū)域,采點高斯。高斯展示方式為 aspectFill. 如下圖:
圖片
粉色部分為高斯圖片的布局。高寬都會溢出被剪切一部分。圖中三角形為畫布中要繪制的點,此點在高斯圖中的位置為 targetY/targetHfill。targetY = (textureCoordinate2.y * drawableH + (targetHFill - drawableH )/float(2)) 為高斯圖被剪裁的上高度+畫布繪制點據(jù)畫布頂部的距離。具體代碼如下:
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 對象。自此,一張高斯背景疊加高清的圖片生成了。在直播流中,不止有一張需要合成,且合成是較耗時操作,需將合成操作放在子線程異步執(zhí)行。執(zhí)行完成后同步回到主線程展示。
e. 在使用 GPUImage 時,有?個需要注意的地?。GPUImage 底層使?的 是 openGL , openGL 在后臺進行渲染會導致 app 崩潰。所以需要我們退出后臺時,停止 openGl 渲染。采取的方式有三種,第?種,將合成隊列在退出后臺前設 置 suspend = yes ,還未開始的合成任務將不再執(zhí)行,直至進入前臺后設 置 suspend = no 。然而,這種方式不能規(guī)避還在進行中的任務。于是第?種,?定 義 NSOperation ,執(zhí)? operation 時。系統(tǒng)會調(diào)用 NSOperation 的 main 函數(shù), 在 main 函數(shù)中,寫實現(xiàn)代碼。在每?句實現(xiàn)代碼前都判斷是否 cancled 。如果 cancled ,直接 return 。在系統(tǒng)即將進?后臺時,將在運行的任務 cancle 掉, operation 繼續(xù)執(zhí)行,監(jiān)測到已經(jīng) cancle 了,就會 return 。然而,粒度還是不夠細。如果任務執(zhí)行到最后一行,且最后一行有 openGL 操作,那么就會攔截不到。第三種, GPUImage 的渲染都是放在??的?個隊列同步執(zhí)?,在接收到系統(tǒng)將進入后臺的通知中,加入渲染隊列?個同步空任務,則系統(tǒng)會執(zhí)行完空任務前的所有 任務之后,再進入后臺。從而避免后臺渲染。
自此,多張合成圖片的方案就結(jié)束了,結(jié)合線上數(shù)據(jù),本頁內(nèi)存消耗節(jié)約10M左右,每張合成圖片時間節(jié)約8ms左右。