? ? ? ? ?剛好端午節(jié)了如输,不想出去旅游看世界了,也不想把玩游戲了肠骆,也不想把妹(ps 其實是我沒有妹子可玩耍??尖坤,不要告訴別人溪窒,你知我知天知就好了???♂?)冬耿,剛好有時間靜下心來寫下在新公司的項目開發(fā)中遇到的問題和一些解決問題的心得舌菜,總結(jié)下來,也算是對那些被bug折磨的郁悶的日子的交代亦镶,同時也希望酷师,遇到同樣問題的你,因為看到這篇文章而少走彎路染乌,因為看到這篇文章而心花怒放,如此懂讯,我也倍感榮幸能幫到你荷憋!?
? ? 話不多說,下面我先簡略簡述下bug的背景和癥狀:
頁面如下:(原諒我對圖片做了必要的涂鴉??)
? ? ? ?一個tableView列表頁褐望,是一個群的成員列表頁面勒庄,UI元素很簡單,包含頭像瘫里、群角色实蔽、拼接的昵稱,原來業(yè)務代碼實現(xiàn)是一次性加載所有需要的數(shù)據(jù)源谨读,數(shù)據(jù)稍微多時局装,頁面卡頓,甚至會閃退劳殖,或者導致其他頁面不可測的卡頓铐尚、閃退,數(shù)據(jù)源大時哆姻,這個查看群組成員的功能基本報廢不能使用宣增,并且易導致app卡頓、閃退矛缨。
? ? ? 你一定會疑問爹脾,這不是及其簡單的頁面嘛帖旨,沒錯,你很聰明灵妨,這的確是極其簡單的頁面效果解阅;但是你看下群的成員是多少,沒錯是將近10萬人的群闷串,10萬瓮钥,也就意味著大量的數(shù)據(jù)源;可能你心里還在想烹吵,10萬人又咋了碉熄,我1000萬的數(shù)據(jù)源還處理過呢,分布加載不就解決問題了嘛肋拔;沒錯锈津,我完全相信你處理過1000萬的數(shù)據(jù)源,我也絕對相信你用分頁加載來控制用戶的行為來避免設備的cpu瞬時峰值過大凉蜂;但是琼梆,兄弟,不要慌窿吩,如你所以為的那樣茎杂,如果這個功能是我從0到1開發(fā),我也會如你所想的那樣分頁加載處理纫雁,可問題是煌往,這個項目是12年就運營的,目前代碼已經(jīng)小百萬行了轧邪,單單這個頁面控制器代碼也堆了足足1.4萬行刽脖,這里面牽涉了UI最終展示前的大量業(yè)務邏輯(比如踢人、新進群忌愚、群成員頭像/昵稱更換曲管、群成員角色變換等大概十來種業(yè)務)和相應業(yè)務操作對應的緩存處理邏輯,看到1.4萬行代碼外加十來種完全不知道的業(yè)務硕糊,兄弟院水,不用告訴我,我也知道你會一頭霧水(ps 技術(shù)溜的上云霄的小伙伴就自動屏蔽了?)癌幕;兄弟衙耕,也不要嗔怪為什么要在一個控制器里寫那么多代碼,接受當前的代碼事實是你應該也是唯一可做的事勺远,至于你看著不爽橙喘,決定要盤古開天劈地來重構(gòu)代碼,這是你接受事實后完全搞清楚這一塊功能后并且和相關(guān)開發(fā)端的同事評估效益比后才能想的事胶逢。眼下厅瞎,這邊領(lǐng)導給你說這問題很嚴重饰潜,要立即修復;那邊產(chǎn)品經(jīng)理過來說和簸,這個問題很嚴重彭雾,市場已經(jīng)嚴重抱怨甚至憤怒了,老板已經(jīng)嚴重關(guān)切這事了锁保,你要立刻馬上現(xiàn)在解決掉薯酝!所以,當前爽柒,你能做的就是梳理原來的業(yè)務代碼吴菠,找到造成卡頓、閃退的關(guān)鍵因素浩村,先臨時性修復這個問題做葵,后期可以再根據(jù)梳理結(jié)果來評估原來的代碼是否需要完全重構(gòu)!這是你最快也是應該唯一能最快解決這問題的態(tài)度心墅,盡管很難酿矢,依然要平靜的呼吸和說話,然后就埋頭讀代碼找坑點怎燥!
? ?暴露了瘫筐,暴露了我曾當過個把月iOS講師的事實??,講個bug解決點铐姚,講了一通在著手讀代碼找坑點前的心理活動和獨白严肪,加的戲份太多了,下面直接貼出來簡略的業(yè)務代碼:(代碼有點長谦屑,前方高能,請做好提前預警??)
// 讀取本地緩存數(shù)據(jù)源
- (void)getCacheContent{
?//?當前線程為主線程
?// 一頓業(yè)務操作略過細節(jié)
?// 從本地緩存讀取數(shù)據(jù) 獲得群組成員列表信息數(shù)組resultArray
NSDictionary* responseObject = [NSDictionary dictionaryWithContentsOfFile:kCachefilePath];?// kCachefilePath 為本地緩存文件地址
? ? NSMutableArray *resultArray = [NSMutableArray arrayWithArray:[responseObject objectForKey:@"info"]];
?// usersInfoDic是一個存放FriendInfoStructure成員個體對象的字典 tableview的數(shù)據(jù)源
?if?(!usersInfoDic) {
? ? ? ? usersInfoDic = [NSMutableDictionary dictionary];
? ? }
?//?tips 1?下面這一頓操作 是遍歷本地群組成員列表并對照本地數(shù)據(jù)庫做查詢對比處理 生成群組成員個人對象FriendInfoStructure 會創(chuàng)建大量對象
?for?(NSDictionary* dic?in?resultArray) {
//autoreleasepool 自動釋放池 能夠讓對象及時的銷毀 避免內(nèi)存峰值過高而閃退 ?這是我規(guī)避內(nèi)存峰值添加的?
//@autoreleasepool?{
? ? ? ? NSInteger userID = [[dic objectForKey:@"uid"] intValue];
? ? ? ? NSString* userIdString = [dic objectForKey:@"uid"];
?// 創(chuàng)建對象
? ? ? ? FriendInfoStructure* infoStructure = [FriendInfoStructure new];
?? ? ? ??infoStructure = [usersInfoDic objectForKey:userIdString];??
?if?(!infoStructure) {
?// 這一步是查詢本地數(shù)據(jù)庫 比對并做必要的一些處理
? ? ? ? ? ? infoStructure =
[self?getCacheInfoFromFMDB:userID];
? ? ? ? ? ? [usersInfoDic setObject:infoStructure forKey:userIdString];? ? ??
}?else?if?(infoVersion != infoStructure.groupInfoVersion) {
? ? ? ? ? ? infoStructure.groupInfoVersion = infoVersion;
?} ? ? ?}
? }
?}
? ? [tableView reloadData];
?// 從網(wǎng)絡上獲取最新的群組成員列表信息
?// 網(wǎng)絡數(shù)據(jù)請求放在這的目的就是 加載本地緩存的同時進行網(wǎng)絡最新數(shù)組加載 用戶先看到緩存數(shù)據(jù) 當網(wǎng)絡最新數(shù)據(jù)加載下來時再刷新 展示最新數(shù)據(jù)
[self?getTableData];
}
// 網(wǎng)絡數(shù)據(jù)源處理和本地數(shù)據(jù)源比對篇梭、更換處理
- (void)getTableData{
?//當前線程為子線程
?//一頓網(wǎng)絡請求的操作 返回字典類型的數(shù)據(jù)responseObject
? ? NSDictionary *responseObject;
?// 將網(wǎng)絡上下載的最新的群組成員信息存儲在本地緩存數(shù)據(jù)中 (注意氢橙,這個過程包含了替換原來的本地緩存數(shù)據(jù))
[responseObject writeToFile:kCachefilePath atomically:YES];
?// 解析到網(wǎng)路上獲取到的最新的群組成員信息列表resultArray
? ? resultArray = [NSMutableArray arrayWithArray:[responseObject objectForKey:@"info"]];
?if?(!usersInfoDic) {
? ? ? ? usersInfoDic = [NSMutableDictionary dictionary];
? ? }
std::vector<int> uid_vector;?// 注意 這是c++代碼?正如中國外交部的聲明一樣 字越少 信息量越大?因為我們項目架構(gòu)是?底層(數(shù)據(jù)層是c++代碼) + 中間層(c++代碼和oc代碼組成,目的是建立抽象層,使底層能兼容PC恬偷、Andriod悍手、Apple三端) + 高層(UI層),因此有c++代碼 這是一個巨坑 后面我會解釋為什么是一個巨坑
?//?tips 11下面的也是遍歷網(wǎng)絡上獲得的數(shù)據(jù) 并和本地緩存數(shù)據(jù)庫比對 會大量創(chuàng)建群組成員個人信息FriendInfoStructure對象
?for?(int?a = 0; a < (int)[resultArray count];
?? ? ? ? a++) {
//autoreleasepool 自動釋放池 能夠讓對象及時的銷毀 避免內(nèi)存峰值過高而閃退 ?這是我規(guī)避內(nèi)存峰值添加的?
//@autoreleasepool?{
? ? ? ? NSDictionary* dic =
? ? ? ? [resultArray objectAtIndex:a];
? ? ? ? NSInteger userID = [[dic objectForKey:@"uid"] intValue];
? ? ? ? NSString* userIdString = [dic objectForKey:@"uid"];
?// ?創(chuàng)建對象
? ? ? ? FriendInfoStructure* infoStructure = [FriendInfoStructure new];
? ? ? ? infoStructure = [usersInfoDic objectForKey:userIdString];
?if?(!infoStructure) {
?// 和本地緩存數(shù)據(jù)庫比對
? ? ? ? ? ? infoStructure =
[self?getCacheInfoFromFMDB:userID];
? ? ? ? ? ? [usersInfoDic setObject:infoStructure forKey:userIdString];? ? ? ? ??
//if?(self.needRequest) {
?self.needRequest =?NO;
uid_vector.push_back(userID);?//?這是上面c++ 代碼對應的業(yè)務 也是字越少 信息量越大
? ? ? // ? ? ? ? ? }? ? ? ??
}?else?if?(infoVersion != infoStructure.groupInfoVersion) {
? ? ? ? ? ? infoStructure.groupInfoVersion = infoVersion;
? ? ? ? }?? ? ? ?
? ? }??
?// 當所有網(wǎng)絡數(shù)據(jù)比對完 刷新UI
? ? [[NSOperationQueue mainQueue]
?? ? addOperationWithBlock:^{
?//?這兩行代碼及其關(guān)鍵 通過中間層觸發(fā)c++底層查詢?nèi)航M成員信息的條件 這是一個很不容易發(fā)現(xiàn)的坑 當初定位到這兩行代碼花費了我五六個小時的時間
?? ? ? ? [[[[AppDelegate appDelegate]
? ? ? ? ? ? sharedProtoEngine] sharedGroupEngine]
getGroupUserCardVecForGId:self.groupId
? ? ? ? ? forUserIdVec:uid_vector];??
?// 這個就是常規(guī)操作 刷新UI
?? ? ? ? [weakTableView reloadData];?? ? ? ?
?? ? }];
}
? ? 下面是我排查的思路:首先入手UI層袍患,然后分析數(shù)據(jù)層
1坦康、從UI層排查
?是個列表tableView,首先找到對應的數(shù)據(jù)源代理诡延,然后找對應的tableViewCell,看下cell內(nèi)部的UI控件繪制是否存在過度繪制滞欠、重復繪制、耗時繪制情況肆良,發(fā)現(xiàn)UI層都是常規(guī)操作筛璧,造成卡頓逸绎、閃退的不可能,當然如果是一直存在刷新的話夭谤,可能會因為cpu超負荷了而卡頓棺牧,但是這種極端情況幾乎不可能發(fā)生,即便發(fā)生了朗儒,cell是被動繪制颊乘,一定是其他地方觸發(fā)了一直刷新,原因在造成不停刷新的地方醉锄。UI的原因暫時沒有可能性
2乏悄、數(shù)據(jù)層排查
最開始,排查數(shù)據(jù)層我沒有采用額外的手段榆鼠,就是在xcode的對應的群組成員信息列表控制器過目下所有的方法纲爸,找到與數(shù)據(jù)源處理相關(guān)的方法,重點關(guān)注數(shù)據(jù)源的set方法入口處和get方法出口處妆够,然后识啦,一眼就看到上面讀取本地數(shù)據(jù)源的方法- (void)getCacheContent表tips 1處存在for循環(huán),然后在for循環(huán)里又執(zhí)行了創(chuàng)建對象的操作神妹,職業(yè)直覺颓哮,很顯然,當數(shù)據(jù)源很多時鸵荠,這里大量創(chuàng)建對象冕茅,并且是主線程中執(zhí)行的,必然的蛹找,數(shù)據(jù)源多時姨伤,for循環(huán)會耗費很長時間,自然出現(xiàn)卡頓庸疾,同時乍楚,由于短時間內(nèi)創(chuàng)建了大量對象,導致設備運行內(nèi)存急劇上升届慈,當時我用6p測試徒溪,當運行內(nèi)存達到650M左右時就閃退了,同時cpu甚至200%的超負荷運行金顿,不卡頓不閃退才怪臊泌!
同理,在- (void)getTableData tips 11處?網(wǎng)絡數(shù)據(jù)請求完也存在這樣的for循環(huán)揍拆,更為驚人的是渠概,這個for循環(huán)也放在主線程了,如此嫂拴,這樣大量消耗內(nèi)存和cpu的操作高氮,必然導致卡頓和閃退了慧妄,在某些數(shù)量的數(shù)據(jù)源區(qū)間里,app不會閃退剪芍,但是造成卡頓塞淹,cpu在設計時也有自己的保護機制,當其持續(xù)處于超負荷工作一段時間后罪裹,即便再進入超低負荷(如10%)運行也會有一定時間的卡頓饱普,直到cpu的保護機制閥值達到了,卡頓才會解除状共,因此當從群成員列表頁面退出后再進入其他頁面套耕,app其他頁面不明原因的卡頓也就不足為奇了!
因此峡继,為了規(guī)避內(nèi)存峰值過高而閃退冯袍,我在for循環(huán)里使用了autoreleasepool 自動釋放池 ,讓每個臨時創(chuàng)建的對象及時的銷毀碾牌,保證app不會內(nèi)存峰值過高康愤;?
?到這問題解決完了嘛,并沒有舶吗,遠著呢征冷!如果你仔細看了,你會發(fā)現(xiàn)誓琼,在for循環(huán)里存在從本地緩存數(shù)據(jù)庫中查找比對字段的操作检激,沒錯,存在這種操作的腹侣,并且是大量的查詢本地數(shù)據(jù)庫叔收,試想一個存有10萬條數(shù)據(jù)的數(shù)據(jù)庫去查找,是多么耗時傲隶,cpu會長時間超負荷工作的今穿,卡頓是必然的,因此伦籍,必須限制數(shù)據(jù)源加載的方式,保證一定時間內(nèi)不能有太多這種操作腮出,因此帖鸦,我改變了原來的數(shù)據(jù)交互方式,采用分頁加載的方式胚嘲,小手機每次加載50條數(shù)據(jù)作儿,大手機每次加載100條數(shù)據(jù),這樣就不會因為本地數(shù)據(jù)庫查詢而導致cpu超負荷工作了馋劈!
? ?到這攻锰,采用自動釋放池技術(shù)和分頁加載的交互方式晾嘶,從理論上,內(nèi)存過高和cpu滿負荷工作的因素都消除了娶吞,貌似可以6的飛起的運行app了垒迂。哼著小曲、滿心歡喜的comman + r 運行妒蛇,感覺自己已經(jīng)溜的飛起机断,從1.4萬行代碼和十多種業(yè)務交叉中竟找到了核心卡點,溜的不行不行的......結(jié)果绣夺,下一秒中吏奸,定睛一看,懷疑了人生陶耍,又卡了奋蔚、又閃退了,只是卡和閃退的時間點滯后了烈钞,不該呀泊碑、不會呀,都這么處理了怎么還不行呢棵磷,抓狂中抓狂中??????......
抓狂過后蛾狗,清醒了,不是卡頓仪媒、閃退滯后了嘛沉桌,說明數(shù)據(jù)源到UI刷新間還有坑點沒找到,說明整個卡頓算吩、閃退背后留凭,除了for循環(huán)和數(shù)據(jù)庫查詢導致外,還有其他原因偎巢!讀代碼中打斷點調(diào)試排查中......就這樣三四小時過去了蔼夜,還是沒定位到關(guān)鍵問題代碼處,這種通過讀代碼和打斷點調(diào)試的方式不行了压昼,效率太低求冷,也不容易找到問題點了。好窍霞,我就打開了xcode自帶的調(diào)試工具instruments匠题,然后然后在xcode --> Product ?--> Profile,打開instruments,選擇Time profile,運行app但金,發(fā)現(xiàn)tableView的reloadData,很長時間一直在執(zhí)行韭山,主線程一直滿負荷運行,沒錯,一定是哪個地方一直在觸發(fā)reloadData钱磅,在刷新相關(guān)處梦裂,通過打斷點、注釋代碼盖淡,終于找到關(guān)鍵代碼年柠,還記得前面我代碼里寫的c++代碼嘛,沒錯禁舷,就是 std::vector<int> uid_vector 和
?? ? ? ? [[[[AppDelegate appDelegate]
? ? ? ? ? ? sharedProtoEngine] sharedGroupEngine]
getGroupUserCardVecForGId:self.groupId
? ? ? ? ? forUserIdVec:uid_vector];??
后者是中間層的代碼彪杉,會調(diào)用底層(c++)獲取群組成員信息的api,然后牵咙,底層以每次10條數(shù)據(jù)返回客戶端派近,客戶端注冊了一個與之對應的觀察者,這樣底層每10條數(shù)據(jù)通知一下洁桌,列表就刷新一下渴丸,直至該群組成員信息下發(fā)完畢才停止下發(fā)數(shù)據(jù),列表才停止刷新另凌;后面的卡頓的原因就是這谱轨,底層數(shù)據(jù)回調(diào)的原因造成,然后就改變uid_vector.push_back(userID)的使用范圍(ps 改變奇執(zhí)行范圍吠谢,上述代碼中只是一部分)土童,至此,相信你也理解我在上面代碼中引用人們評價中國外交部的名言了:字越少 信息量越大 工坊,正是c++和oc語言的面向?qū)ο蟮奶匦韵缀梗阒荒芸吹綄ο笞罱K的使用,具體細節(jié)你不知道王污,當你知道你作用時罢吃,一切都好用,像我剛遇到的那樣昭齐,剛?cè)胧猪椖磕蛘校且徊糠謈++?代碼業(yè)務邏輯都沒人知道,自己入坑了也渾然不知阱驾,直到疼了才發(fā)現(xiàn)是入坑了就谜,這也是沒辦法的辦法,尤其對一些比較老且業(yè)務復雜的項目里覆,中間都不知換了多少波人了丧荐,很多代碼都沒人知道了,如此租谈,這樣只能自己先進坑,疼了再修復出坑了,沒辦法割去,古老的項目就是這樣的窟却,所以,老項目的問題修復及其的漫長且艱難郁悶呻逆。
??以上就是前段時間遇到的一個比價典型的復合型bug夸赫,同一個問題現(xiàn)象,但是導致其出現(xiàn)的原因是一層又一層的咖城,就這個卡頓茬腿、閃退bug,聽產(chǎn)品經(jīng)理和技術(shù)老大說宜雀,已經(jīng)困繞了他們幾年了切平,只從用戶量過百萬后就一直存在,但是一直沒人能耐心處理掉.....哦辐董,好吧悴品,暴露了,我承認我太老實了??简烘,他們竟然把我忽悠進坑給他們填坑了苔严,怪不的他們都把這個問題都立項了,修復完后孤澎,我才恍然大悟届氢,原來坑這么大,他們讓我成功入坑覆旭,還好我修復了成功出坑了???♂?
? ? 如果你能有耐心讀到這退子,并且還能保持著共鳴,我相信你在處理一些問題時姐扮,會有一個比較清晰的思路和心理預期絮供;再次感謝你耐心讀完這篇文章,寫的有點不像技術(shù)文章了??茶敏,倒像一篇bug產(chǎn)生到毀滅的回憶錄了??