移動CRM影響批發分銷的3種方式
982
2022-05-30
在編寫日常業務代碼時,或多或少都會引入一些導致內存泄漏的代碼,而這種行為又很難被監控,這就導致應用內存泄漏的口子越開越大,直接影響到線上應用的穩定性。
雖然 Xcode 的 Instrucment 提供了 Leaks 和 Allocations 工具能精準地定位內存泄漏問題,但是這種方式相對比較繁瑣,需要開發人員頻繁地去操作應用界面,以觸發泄漏場景,所以 Leaks 和 Allocations 更加適合定期組織的大排查,作為監測手段,則顯得笨重。
對于內存泄漏的監測,業內已經有了兩款成熟的開源工具,分別是 PLeakSniffer 和 MLeaksFinder。
PLeakSniffer 使用 Ping-Pong 方式監測對象是否存活,在進入頁面時,創建控制器關聯的一系列對象代理,根據這些代理在控制器銷毀時能否響應 Ping 判斷代理對應的對象是否泄漏。
MLeaksFinder 則是在控制器銷毀時,延遲 3s 后再向監測對象發送消息,根據監測對象能否響應消息判斷其是否泄漏。
PLeakSniffer 和 MLeaksFinder 這兩個基本能覆蓋大部分對象泄漏或者延遲釋放的場景,考慮到性能損耗以及內存占用因素,個人更偏向于第二種方案。
個人使用 MLeaksFinder,還存在以下問題:
沒有處理集合對象;
沒有處理對象持有的屬性;
每個對象都觸發 3s 延遲機制,沒有緩存后統一處理;
檢測結果輸出分散。
PLeakSniffer 存在以下問題:
沒有處理集合對象;
處理對象持有屬性時,系統類過濾不全面;
處理對象持有屬性時,通過 KVC 訪問屬性導致一些懶加載的觸發;
無法處理未添加到視圖棧中的泄漏視圖;
檢測結果輸出分散。
對于檢測到泄漏對象的交互處理,兩者都提供了終端 log 輸出和 alert 提示功能,MLeaksFinder 甚至可以直接通過斷言中斷應用,這種提示在開發階段尚可接受,但是在提測階段,強交互會給測試人員造成困擾。至于為什么在提測階段還要集成泄漏監測工具,主要有兩個原因:
應用功能過多的情況下,開發人員無法兼顧到老頁面,一些老頁面的泄漏場景可以通過測試人員在測試時觸發,收集之后再統一處理;
在組件化開發環境下,開發人員可能并沒有集成泄漏監測工具,這種情況下,需要在提測階段統一收集沒有解決的泄漏問題。
因此,對于監測輸出的訴求有兩點:
開發時,通過終端日志提示開發者出現了內存泄漏;
提測時,收集內存泄漏的信息并上傳至效能后臺,統一分配處理;
和 MLeaksFinder 一樣,選擇延遲 3s 的機制來判斷對象是否泄漏,但是實現的細節略有差別。首先,監測入口變更為 viewDidDisappear: 方法,只需在控制器被父控制器中移除或者被 Dismissed 時,觸發監測動作即可:
- (void)LeaksMonitor_viewDidDisappear:(BOOL)animated { [self LeaksMonitor_viewDidDisappear:animated]; if (![self isMovingFromParentViewController] && ![self isBeingDismissed]) { return; } [[YDWLeaksMonitor shared] detectLeaksForObject:self]; }
1
2
3
4
5
6
7
8
9
在應用中,還有一種監測入口出現在變更根控制器時,由于直接設置根控制器不會觸發 viewDidDisappear 方法,所以需要另外設置 :
- (void)LeaksMonitor_setRootViewController:(UIViewController *)rootViewController { if (self.rootViewController && ![self.rootViewController isEqual:rootViewController]) { [[YDWLeaksMonitor shared] detectLeaksForObject:self.rootViewController]; } [self LeaksMonitor_setRootViewController:rootViewController]; }
1
2
3
4
5
6
7
為了能夠統一處理控制器及其持有對象,可以像 PLeakSniffer 一樣,給每個對象包裝一層代理 :
@interface YDWLeakObjectProxy : NSObject // 持有 target 的對象弱引用 @property (weak, nonatomic) id host; // 被 host 持有的對象弱引用 @property (weak, nonatomic, readonly) id target; @end
1
2
3
4
5
6
只要 host 釋放了而 target 沒釋放,則視 target 已泄漏,如果 host 未釋放,則不檢測 target,然后使用一個 collector 去收集這些對象對應的 proxy ,在收集完之后統一監測 collector 中的所有 proxy ,這樣就可以在一個控制器監測完成后,統一上傳監測出的泄漏點 :
- (void)detectLeaksForObject:(id
1
2
3
4
5
6
7
8
9
10
11
12
因為要對不同的類做特異化處理,因此先定義一個協議,通過這個協議中的 collect 方法去收集不同類實例化對象的 proxy :
@protocol YDWLeakObjectProxyCollectable
1
2
3
4
5
6
7
8
9
關鍵在于如何讓 NSObject 實現此協議,主要有四個步驟 :
過濾系統類調用;
向 collector 添加封裝的 proxy;
循環遍歷對象對應的非系統類 / 父類屬性,找出 copy / strong 類型屬性,并獲取其對應的成員變量值;
向收集的所有成員變量對象發送 collect 方法。
NSObject 實現 collect 協議方法后,其子類就可以通過這個方法遞歸地收集名下需要監測的屬性信息。比如對于集合類型 NSArray ,實現協議方法如下,表示收集自身和每個集合元素的信息,不過由于 NSArray 是系統類,所以其實例化對象并不會被收集進 collector ,如果要收集系統類的屬性信息,只能通過讓系統類實現協議并重載 collect 方法,手動向屬性值發送 collect 消息實現,UIViewController 的 childViewControllers、presentedViewController、view 屬性也同理 :
- (void)LeaksMonitor_collectProxiesForCollector:(YDWYDWLeakObjectProxyCollector *)collector withContext:(YDWLeakContext *)ctx { [super LeaksMonitor_collectProxiesForCollector:collector withContext:ctx]; [self enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { if ([obj conformsToProtocol:@protocol(YDWLeakObjectProxyCollectable)]) { [obj LeaksMonitor_collectProxiesForCollector:collector withContext:LM_CTX_D(ctx, @"contains")]; } }]; }
1
2
3
4
5
6
7
8
9
需要注意的是,直接調用屬性的 getter 方法獲取屬性值,可能會觸發屬性懶加載,導致出現意料之外的問題 (比如調用 UIViewController 的 view 會觸發 viewDidLoad),所以要通過 object_getIvar 去獲取屬性對應的成員變量值。當然,這種處理方式會導致無法收集某些沒有對應成員變量值的屬性,比如關聯對象、控制器的 view 等屬性,權衡利弊之后,可以選擇忽略這種屬性的監測。
除了收集必要的對象信息之外,我還記錄了監測對象的引用路徑信息,也就是上面 LM_CTX_D 宏做的事情。有些情況下,對象的引用路徑能幫助我們發現,路徑上的哪些操作導致了對象的泄漏,特別是在網頁上瀏覽泄漏信息時,如果只有泄漏對象類和引用泄漏對象類兩個信息,脫離了對象泄漏時的上下文環境,會增加修復的難度。有了引用路徑信息后,輸出的泄漏信息如下 :
[ O : YDWViewController.view->UIView.subviews->__NSArrayM(contains)->A.subviews->__NSArrayM(contains)->O YDWViewController : YDWViewController.childViewControllers->YDWViewController __NSCFTimer : YDWViewController.timer->__NSCFTimer ]
1
2
3
4
5
系統類信息并不是需要關心的,過濾掉并不會影響到最終的監測結果。目前我嘗試了兩種方式來確定一個類是否為系統類:
通過類所在 NSBundle 的路徑;
通過類所在地址。
第一種的邏輯較為簡單,代碼如下:
BOOL LMIsSystemClass(Class cls) { NSBundle *bundle = [NSBundle bundleForClass:cls]; if ([bundle isEqual:[NSBundle mainBundle]]) { return NO; } static NSString *embededDirPath; if (!embededDirPath) { embededDirPath = [[NSBundle mainBundle].bundleURL URLByAppendingPathComponent:@"Frameworks"].absoluteString; } return ![bundle.bundlePath hasPrefix:embededDirPath]; }
1
2
3
4
5
6
7
8
9
10
11
12
13
應用的主二進制文件,和開發者添加的 embeded frameworks 都會在固定的文件目錄下,所以直接比對路徑前綴即可。
第二種方式的實現步驟如下:
遍歷所有的 image ,通過 image 的名稱判斷是否為系統 image;
緩存所有系統 image 的起始位置,也就是 mach_header 的地址;
判斷類是否為系統類時,使用 dladdr 函數獲取類所在 image 的信息,通過 dli_fbase 字段獲取起始地址;
比對 image 的起始地址得知是否為系統類。
實際嘗試下來后,發現第二種方式耗時會比第一種多,dladdr 函數占用了大部分時間(內部會遍歷所有 image 的開始結束地址,和傳入的地址進行比對),所以最終選擇了第一種方式作為判斷依據。
過濾系統類時,針對那種會自泄漏的對象,需要進行特殊處理,不予過濾。比如 NSTimer / CADisplayLink 對象的常見內存泄漏場景,除了 target 強引用控制器造成循環引用域外,還有一種是打破了循環引用但沒有在控制器銷毀時執行 invalidate 操作,因為 NSTimer 由 RunLoop 持有,不手動停止的情況下,就會造成泄漏。
基于延時的內存泄漏監測機制雖然適用于大部分視圖、控制器和一般屬性的泄漏場景,但是還有少部分情況,這種機制無法處理,比如單例對象和共享對象。
首先說下單例對象,假設有 singleton 屬性,其 getter 方法返回 Singleton 單例,這時延時監測機制無法自動過濾這種情況,依然會認為 singleton 泄漏了。有一種檢測屬性返回值是否為單例的方法,就是向返回值對應類發送 init 或者 share 相關方法,通過方法返回值和屬性返回值的對比結果來判斷,但是事實上我們無法確定業務方的單例是否重寫了 init,也無法獲知具體的單例類方法,所以這種方案適用面比較局限。單例對象的處理,目前還是通過白名單的方式處理較為穩妥。
共享對象的應用場景就比較普遍了,比如現有 A,B 頁面,A 頁面持有模型 M ,在跳轉至 B 頁面時,會將 M 傳遞給 B ,B 強引用了 M ,當 B 銷毀時, M 不會銷毀,而 M 又是 B 某個屬性的值,所以監測機制會判斷 M 泄漏了,實際上 M 只是 A 傳遞給 B 的共享對象。在一個控制器做完檢測就需要上傳至效能后臺的情況下,共享對象還沒有很好的處理方法,后期考慮結合 FBRetainCycleDetector 查找泄漏對象的循環引用信息,然后一并上傳至效能后臺,方便排查這種情況。因為每次 pop 都使用 FBRetainCycleDetector 檢測控制器會比較耗時、甚至會造成延遲釋放和卡頓,所以先用延時機制找出潛在的泄漏對象,再使用 FBRetainCycleDetector 檢測這些泄漏對象,能極大得減少需要處理的對象數量。最終網頁呈現的效果如下:
像內存泄露這種問題,最好在應用初期就開始著手監測和解決,否則當應用功能代碼逐漸增多后,回過頭來處理這種問題費時費力,還是比較麻煩的。
基于 PLeakSniffer 和 MLeaksFinder 監測工具的基礎上,結合團隊業務情況,進行了一些的改造,添加了集合對象的處理、引用路徑的記錄、對象的統一檢測等功能,優化了部分有問題的代碼,在一定程度上提升了延時機制的可用性。
Image iOS
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。