語音社交app開發,如何實現介面優化?
在語音社交app開發中經常會出現卡頓的現象(丟幀),給使用者的感覺很不好。那麼這個現象是怎樣產生的,如何檢測到掉幀,要怎樣去優化呢?本文將針對這幾個問題進行分析
介面渲染流程
在語音社交app開發的介面的渲染過程中CPU和GPU起了比較重要的作用
CPU與GPU
CPU全名是Central Processing Unit(中央處理器),語音社交app開發在載入資源、物件的建立和銷燬、物件屬性的調整、佈局計算、Autolayout、文字渲染,文字的計算和排版、圖片格式轉碼和解碼、影像的繪製(Core Graphics)時,都需要依賴CPU來執行
GPU全名是Graphics Processing Unit(影像處理器),它是一個專門為圖形高併發計算而量身定做的處理單元,比CPU使用更少的電來完成工作並且GPU的浮點計算能力要超出CPU很多。
在語音社交app開發中,相對於CPU來說,GPU能幹的事情比較單一:接收提交的紋理(Texture)和頂點描述(三角形),應用變換(transform)、混合(合成)並渲染,然後輸出到螢幕上。通常你所能看到的內容,主要也就是紋理(圖片)和形狀(三角模擬的向量圖形)兩類。GPU的渲染效能要比CPU高效很多,同時對系統的負載和消耗也更低一些
在語音社交app開發中,我們應該儘量讓CPU負責主執行緒的UI調動,把圖形顯示相關的工作交給GPU來處理,當涉及到光柵化等一些工作時,CPU也會參與進來。
渲染過程
在講渲染原理之前先介紹下CRT顯示器原理。
CRT的電子槍從上到下逐行掃描,掃描完成後顯示器就呈現一幀畫面。然後電子槍回到初始位置進行下一次掃描。為了同步顯示器的顯示過程和系統的視訊控制器,顯示器會用硬體時鐘產生一系列的定時訊號。
當電子槍換行進行掃描時,顯示器會發出一個 水平同步訊號(horizonal synchronization),簡稱HSync;
而當語音社交app開發中一幀畫面繪製完成後,電子槍回覆到原位,準備畫下一幀前,顯示器會發出一個 垂直同步訊號(vertical synchronization),簡稱VSync。顯示器通常以固定頻率進行重新整理,這個重新整理率就是VSync訊號產生的頻率。
CRT的電子槍掃描過程如下圖所示:
雖然現在的顯示器基本都是液晶螢幕了,但其原理基本一致。
介面渲染的流程如下:CPU計算 -> GPU渲染 -> 幀緩衝區 -> 視訊控制器讀取幀緩衝區的資料 -> 顯示器,如下圖:
雙緩衝機制+VSync
如果GPU渲染後,因為某些原因沒有存入語音社交app開發的幀緩衝區,這樣就形成了等待,為了解決了這個問題,就產生了雙緩衝機制,也就是前幀和後幀。
當GPU渲染完一幀後就會存入幀快取區,然後視訊控制器去讀取緩衝幀快取,同時GPU去渲染另一幀並存入另一個幀快取區,這樣來回的切換讀取來顯示介面,如下圖:
這個切換也不是任意時間切的,我們常見的都是一秒渲染60幀,也就是VSync每隔16.67ms傳送一次訊號
所以,語音社交app開發中的視訊控制器會按照VSync訊號逐幀讀取幀緩衝區的資料
卡頓
卡頓產生原理
我們知道VSync每隔16.67ms傳送一次訊號,兩次傳送訊號之間cpu進行了計算,gpu渲染後存入幀緩衝區,那麼如果計算的步驟花的時間比較長,那麼存入幀快取的渲染部分就比較少了,當視訊控制器讀取幀快取時沒有讀全,此時語音社交app開發就產生了丟幀,也就是卡頓。
卡頓過程如下圖:
卡頓檢測
每秒渲染幀數用FPS(Frames Per Second)來表示,通常60fps為最佳,我們可以檢測語音社交app開發的FPS來觀察語音社交app開發是否流暢。
可以使用以下幾種方式檢測語音社交app開發的FPS:
CADisplayLink
語音社交app開發系統在每次傳送VSync時,就會觸發CADisplayLink,我們可以統計每秒傳送VSync的數量來檢視App的FPS是否穩定,主要程式碼如下:
@interface ViewController () @property (nonatomic, strong) CADisplayLink *link; @property (nonatomic, assign) NSTimeInterval lastTime; // 每隔1秒記錄一次時間 @property (nonatomic, assign) NSUInteger count; // 記錄VSync1秒內傳送的數量 @end self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkAction:)]; [_link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; - (void)linkAction: (CADisplayLink *)link { if (_lastTime == 0) { _lastTime = link.timestamp; return; } _count++; NSTimeInterval delta = link.timestamp - _lastTime; if (delta < 1) return; _lastTime = link.timestamp; float fps = _count / delta; _count = 0; NSLog(@"? fps : %f ", fps); }
RunLoop
在 Runloop原理 中,我們詳細的分析了Runloop,它的退出和進入實質都是Observer的通知,我們可以監聽Runloop的狀態,並在相關回撥裡傳送訊號,如果在語音社交app開發設定的時間內能夠收到訊號說明是流暢的;如果在設定的時間內沒有收到訊號,說明發生了卡頓。具體實現如下:
@interface WSBlockMonitor () { CFRunLoopActivity activity; } @property (nonatomic, strong) dispatch_semaphore_t semaphore; @property (nonatomic, assign) NSUInteger timeoutCount; @end - (void)registerObserver{ CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL}; //NSIntegerMax : 優先順序最小 CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, NSIntegerMax, &CallBack, &context); CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes); } static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { WSBlockMonitor *monitor = (__bridge WSBlockMonitor *)info; monitor->activity = activity; // 傳送訊號 dispatch_semaphore_t semaphore = monitor->_semaphore; dispatch_semaphore_signal(semaphore); } - (void)startMonitor { // 建立訊號 _semaphore = dispatch_semaphore_create(0); // 在子執行緒監控時長 dispatch_async(dispatch_get_global_queue(0, 0), ^{ while (YES) { // 超時時間是 1 秒,沒有等到訊號量,st 就不等於 0, RunLoop 所有的任務 long st = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC)); if (st != 0) { if (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting) // 即將處理sources,即將進入休眠 { if (++self->_timeoutCount < 2) { NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount); continue; } // 一秒左右的衡量尺度 很大可能性連續來 避免大規模列印! // 可在此處記錄卡頓堆疊資訊,進行排查 NSLog(@"檢測到超過兩次連續卡頓"); } } self->_timeoutCount = 0; } }); } // 呼叫方法 - (void)start{ [self registerObserver]; [self startMonitor]; }
主要在主執行緒監聽Runloop即將處理事物和即將休眠兩種狀態,子執行緒監控時長,如果連續兩次1秒內沒有收到訊號,說明發生了卡頓,此時可以記錄卡頓的堆疊以便於排查。
微信matrix
微信的matrix也是藉助runloop實現的,大體流程上面Runloop相同,它使用退火演算法優化捕獲卡頓的效率,防止連續捕獲相同的卡頓,並且通過儲存最近的20個主執行緒堆疊資訊,獲取最近最耗時堆疊。所以需要準確的分析語音社交app開發卡頓原因可以藉助微信matrix來分析卡頓。
滴滴DoraemonKit
DoraemonKit的卡頓檢測方案並沒有對runloop操作,它也是while迴圈中根據一定的狀態判斷,通過語音社交app開發的主執行緒中不斷對傳送訊號semaphore,迴圈中等待訊號的時間為5秒,等待超時則說明主執行緒卡頓,並進行相關上報。
優化方案
在檢測到語音社交app開發卡頓後,接下來就應該去進行相關的優化,我們可以通過以下幾種方案
預排版
在語音社交app開發中,佈局我們通常選擇用Masonry或SnapKit,他們都是基於AutoLayout來實現的,自動佈局通常在賦值過後才決定UI控制元件的大小。而根據蘋果的介紹,相對於AutoLayout,frame產生的消耗要小的多
例如在複雜結構的UITableViewCell中,賦值過後會產生UI控制元件的大小,如果cell比較多會進行頻繁的計算,這樣就會消耗效能。這種情況我們可以在請求完資料時,就計算好各個控制元件的Rect,然後在資料賦值時,也將各個控制元件的frame進行賦值,這樣會大大減少計算。這個方案也叫做預排版,具體程式碼如下:
資料DataModel程式碼
@interface DataModel : NSObject @property (nonatomic, strong) NSString *name; @end
佈局LayoutModel程式碼
// .h @interface LayoutModel : NSObject @property (nonatomic, assign) CGRect nameRect; @property (nonatomic, strong) DataModel *data; @property (nonatomic, assign) CGFloat height; - (instancetype)initWithModel:(DataModel *)model; // 初始化程式碼 @end // .m - (instancetype)initWithModel:(DataModel *)model { self = [super init]; if (self) { self.data = model; // 根據資料計算相關控制元件的size CGSize size = [self getSizeWithContent:model.name]; self.nameRect = CGRectMake(15, 100, size.width, size.height); self.height = 200; // 計算cell高度 } return self; } - (CGSize)getSizeWithContent: (NSString *)content { //根據文字計算size ... return CGSizeMake(100, 40); }
layoutModel初始化時,要傳入對應的model,然後根據model中的相關欄位計算相應的size,然後講相應的rect賦值給layoutModel中的相關欄位。
cell程式碼
// .h - (void)configCellWithModel: (LayoutModel *)model; // .m @interface TestCell () @property (nonatomic, strong) UILabel *nameLbl; @end @implementation TestCell - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { self.nameLbl = [UILabel new]; [self.contentView addSubview:_nameLbl]; } return self; } - (void)configCellWithModel:(LayoutModel *)model { self.nameLbl.frame = model.nameRect; // frame 賦值 self.nameLbl.text = model.data.name; // 資料賦值 }
cell中的控制元件建立完後設定相關的顏色字型,然後在資料賦值時同時對frame進行賦值
VC程式碼
@property (nonatomic, strong) NSMutableArray<LayoutModel *> *dataSource; // 模擬網路請求 dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSDictionary *dataDic; // 網路請求資料 NSMutableArray<DataModel *> *modelArray = [NSMutableArray array]; for (NSDictionary *dic in dataDic[@"data"]) { DataModel *model = [DataModel yyModel: dic]; // 相關的json轉model方法 [modelArray addObject:model]; } self.dataSource = [NSMutableArray arrayWithCapacity:modelArray.count]; for (DataModel *model in modelArray) { LayoutModel *layout = [[LayoutModel alloc] initWithModel:model]; // 根據資料model,初始化layoutModel,並進行控制元件的layout計算 [self.dataSource addObject:layout]; } // 計算完成後reloadData dispatch_async(dispatch_get_main_queue(), ^{ [self.tableView reloadData]; }); }); - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return _dataSource.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSString *identifier = @"cellID"; TestCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; if (!cell) { cell = [[TestCell alloc] initWithStyle:(UITableViewCellStyleDefault) reuseIdentifier:identifier]; } [cell configCellWithModel:self.dataSource[indexPath.row]]; return cell; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { LayoutModel *model = self.dataSource[indexPath.row]; return model.height; }
vc中主要是網路請求後建立資料model後,然後根據將dataModel建立layoutModel並進行相關計算,完成後再reloadData,這樣就一次性將相關的佈局計算好,滑動cell時只是進行賦值,無需額外的佈局耗時計算
預解碼&預渲染
預解碼主要是對影像視訊類進行優化,例如UIImage,它的載入流程如下:
Data Buffer(資料緩衝區)解碼後快取到Image Buffer(影響緩衝區),然後存入幀緩衝區再進行渲染。
語音社交app開發解碼的過程是比較消耗資源的,所以可以將解碼工作放到子執行緒,提前做好一些解碼工作
圖片解碼具體的做法可以參考SDWebImage中的處理,而音視訊的解碼可以參考FFmpeg
按需載入
按需載入顧名思義就是語音社交app開發需要時再載入,例如TableView,在滑動時每出現一個cell就會走cellForRow裡的賦值方法,有些cell剛出現後又馬上在介面消失,像這種可以監聽滑動的狀態,當滑動停止時根據tableView的visibleCells獲取當前可見cell,然後對這些cell進行賦值,這樣也節省了很多的開銷。
非同步渲染
非同步渲染就是在語音社交app開發中子執行緒把需要繪製的圖形提前處理好,然後將處理好的影像資料直接返給主執行緒使用,這樣可以降低主執行緒的壓力。
非同步渲染操作的是layer層,將展示的內容通過UIGraphics畫成一張image然後展示在layer.content上
我們知道繪製會執行drawRect:方法,在方法中檢視堆疊得知:
在堆疊中得知CALayer在呼叫display方法後回去呼叫繪製相關的方法,根據流程我們來實現一個簡單的繪製:
// WSLyer.m - (void)display { // 建立context CGContextRef context = (__bridge CGContextRef)[self.delegate performSelector:@selector(createContext)]; [self.delegate layerWillDraw:self]; // 繪製的準備工作 [self drawInContext:context]; //繪製 [self.delegate displayLayer:self]; // 展示點陣圖 [self.delegate performSelector:@selector(closeContext)]; // 結束繪製 } // WSView.m - (void)drawRect:(CGRect)rect { // Drawing code } + (Class)layerClass { return [WsLayer class]; } // 建立context - (CGContextRef)createContext { UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.layer.opaque, self.layer.contentsScale); CGContextRef context = UIGraphicsGetCurrentContext(); return context; } - (void)layerWillDraw:(CALayer *)layer { // 繪製的準備工作 } - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx { [super drawLayer:layer inContext:ctx]; // 形狀 CGContextMoveToPoint(ctx, self.bounds.size.width / 2- 20, 20); CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 + 20, 20); CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 + 40, 60); CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 - 40, 60); CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 - 20, 20); CGContextSetFillColorWithColor(ctx, UIColor.magentaColor.CGColor); CGContextSetStrokeColorWithColor(ctx, UIColor.magentaColor.CGColor); // 描邊 CGContextDrawPath(ctx, kCGPathFillStroke); // 文字 [@"無雙" drawInRect:CGRectMake(self.bounds.size.width / 2 - 40, 70, 80, 24) withAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:15],NSForegroundColorAttributeName: UIColor.blackColor}]; // 圖片 [[UIImage imageNamed:@"buou"] drawInRect:CGRectMake(self.bounds.size.width / 2 - 40, 100, 60, 50)]; } // 主執行緒渲染 - (void)displayLayer:(CALayer *)layer { UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); dispatch_async(dispatch_get_main_queue(), ^{ layer.contents = (__bridge id)(image.CGImage); }); } // 結束context - (void)closeContext { UIGraphicsEndImageContext(); }
在VC中只需要新增view物件即可
// ViewController.m WsView *view = [[WsView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)]; view.backgroundColor = UIColor.yellowColor; [self.view addSubview:view];
執行結果和層級關係如下:
在語音社交app開發中非同步渲染處理起來相對要複雜些,具體的實踐可以參照其他非同步渲染框架。
本文轉載自網路,轉載僅為分享乾貨知識,如有侵權歡迎聯絡雲豹科技進行刪除處理
原文連結:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69996194/viewspace-2846526/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 實現語音社交原始碼介面效能優化,從索引入手原始碼優化索引
- 婚戀app原始碼開發,如何實現介面效能優化?APP原始碼優化
- 社交app開發功能,社交軟體開發功能,社交app,社交軟體。APP
- 為何語音社交app原始碼的開發始終都這麼火爆?APP原始碼
- 語音交友app開發,點選按鈕出現彈窗的實現方式APP
- 語音社交原始碼開發,兩個執行緒按照指定方式有序相交的實現原始碼執行緒
- 如何實現 iOS App 的冷啟動優化iOSAPP優化
- 小說系統原始碼開發,如何優雅的實現對外介面?原始碼
- APP開發實戰119-APP程式碼優化APP優化
- VC開發多語言介面支援----INI實現(轉)
- 流量變現白皮書:社交APP如何實現營收閉環APP營收
- 如何實現 OpenAPI 多語言 SDK 開發?API
- Android開發優化之——對介面UI的優化(1)Android優化UI
- Android開發優化之——對介面UI的優化(2)Android優化UI
- Android開發優化之——對介面UI的優化(3)Android優化UI
- CI Weekly #2 | 如何優化開發流程,實現專案持續整合?優化
- java實現zabbix介面開發Java
- 使用APICloud開發App的效能優化探索——合理使用同步/非同步介面APICloudAPP優化非同步
- android開發實現記憶體優化功能Android記憶體優化
- 語音介面2.0
- 語音喚醒實現
- 語音社交平臺受到關注 語音社交平臺會是新風口嗎?
- 如何實現婚戀app原始碼中直播首屏載入優化?APP原始碼優化
- 跨過網際網路創業鴻溝,語音社交app持續火爆社交市場創業APP
- 短視訊app開發,介面滑動到底的幾種實現方式APP
- 語音社交系統的亮點在哪,未來行業發展趨勢如何?行業
- java 語音用xml檔案實現圖形介面 xml檔案JavaXML
- 如何用前端實現麥克風語音喚醒前端
- 如何發現優秀的IT開發人員
- 遊戲陪玩系統原始碼開發,如何實現圖片和動畫的優化?遊戲原始碼動畫優化
- 如何優化產品開發過程?優化
- C# 實現語音聊天C#
- App 開發語言APP
- 網易雲音樂介面+vue全家桶開發一款移動端音樂webAppVueWebAPP
- IM即時通訊聊天社交APP VX 聊天語音視訊系統APP
- Android音訊開發之AudioRecord錄音實現Android音訊
- 如何更好的使用OPcache實現效能優化opcache優化
- 如何實現一個模板引擎二:優化優化