好久沒有做記錄了,前段時間版本忒忙,最近家人狀況抱恙,只能忙里偷閑記錄兩
個挺有意思的小Tip.包含了解決思路和最終的方案.也方便有遇上同樣問題的同學解決問題.
NSCache的鬼
某一天,在bug系統(tǒng)中看到了一個Ticket,是這樣記錄的:
頁面打開后,會閃爍.
??這是幾個意思?于是按照描述,進行的bug復現(xiàn).明白了閃爍是什么意思.
QA所謂的閃爍,是指的頁面每次進入有圖片的地方,會進行一次閃爍.也就是圖片的刷新.
修復這個bug,可以有2個方法.一是解決"每次"這個問題,使得頁面只有手動刷新的時候才進行圖片的處理.二是找清楚問題的本質(zhì),為什么使用了緩存(sdwebimage
),仍然要"閃爍".
對于方法一,當然是不能接受的.一個是業(yè)務需要,另一個是沒有本質(zhì)上解決問題.于是開始查詢緩存的問題.
首先進行一個判定,是否是緩存引起的.于是稍微修改下代碼,把網(wǎng)絡圖片替換成了本地圖片,看看現(xiàn)象是否仍然存在:果然消失了,于是確定是緩存問題.
其次跟蹤代碼,查詢是否是sd緩存被清除:通過斷點調(diào)試,很容易知道,的確是緩存被清空了.從sd的內(nèi)存緩存中拿不到相應的key對應的圖片.
然后查詢代碼,查詢是否有以下2種簡單的可能:
- 是否調(diào)用了
[[SDImageCache sharedImageCache] clearMemory]
之類的相關代碼 - 是否對sd進行了相關設置,比如max size/limit/age等.
然而通過查詢,沒有類似的相關代碼.經(jīng)過一番瞎搞,似乎沒什么轍了.
再想想,復現(xiàn)步驟有一步:按下home鍵再進入
.難道和生命周期相關?不過不好排查,原因是:
- 操作步驟包含多個生命周期,例如
enter background
,enter forgeground
. - 除了
app delete
中的代理以外,還包含了分散在整個項目內(nèi)的通知,并且還有較多的他人模塊.
不過麻煩也得做,從簡到難.首先排除app delegate
中是否有影響:果斷注釋掉,不過現(xiàn)象仍然存在.
然后再來排除通知:這個難度就比較大了,注冊的地方太多,再加上幾種通知...
沒辦法了么?想了想,我們?nèi)绻粨Q了通知,攔截我們需要的通知.這樣就不會發(fā)送生命周期相關的通知,注冊的地方就全部失效了.是不是能知道些什么呢?
幾行代碼搞定,攔截了涉及到的幾個生命周期.最后發(fā)現(xiàn)是enter background
這個生命周期搞的鬼.
到現(xiàn)在為止,也就是發(fā)現(xiàn)了在sd中,一旦enter background
就會清除緩存.于是我們猜測,是sd故意這么寫么?這不科學啊.
追蹤到sd的源碼,內(nèi)存緩存(memory cache),就是一個NSCache
.于是給sd的清除內(nèi)存緩存的方法打上斷點,看看在進入后臺時是否會執(zhí)行:然而答案是令人失望的,并沒有.也就是說,sd本身并沒有做這個操作.
這就詭異了,項目中沒有類似的操作,sd沒有,那...難道是系統(tǒng)的?
如果是系統(tǒng)的,那就是系統(tǒng)調(diào)用了這個NSCache的清除方法.因為是全頁面的閃爍,也就是全部緩存都被清除,而不是針對某一個緩存.那么看看NSCache
的方法,自然就懷疑到了removeAllObjects
這個家伙身上.
于是再次使用run time
賦予我們的神器:交換,來驗證我們的想法:NSCache
在進入后臺的時候,會自動的刪除相關的value,調(diào)用removeAllObjects
這個方法.
Hook了removeAllObjects
方法,答案水落石出.
Well done,果然是這樣的.原因找出來了.剩下的事情就簡單了,調(diào)查下具體是怎么一回事.
原來有這么個協(xié)議:NSDiscardableContent
.這個協(xié)議一共有4個required的方法:
@protocol NSDiscardableContent
@required
- (BOOL)beginContentAccess;
- (void)endContentAccess;
- (void)discardContentIfPossible;
- (BOOL)isContentDiscarded;
@end
這個協(xié)議決定了存儲在NSCache
中的value的一些特性.NSCache
會在進入后臺的時候?qū)ζ渲写鎯Φ乃衯alue按照協(xié)議的方式進行保留或者刪除(discard),如果沒有實現(xiàn)該協(xié)議,則默認刪除.
至于這個bug,大不了就是對image實現(xiàn)個category,category實現(xiàn)了該協(xié)議,然后操作代理方法,根據(jù)業(yè)務邏輯來進行判定即可.
Remote Module
組件化是吵了好久的話題了.雖然我愛湊個熱鬧,看看各家的吵吵鬧鬧:道理是越辯越明,從方便項目上升到架構的藝術.不過實在沒有太大興趣,也不敢擅自重復造輪子.但是最近實在是被部分歷史代碼折騰煩了,不得不也開始組件化的道路.
套路還是那個套路,沒有什么新意.基本按照casa的Mediator
走.只是有一點思考:
是否要注冊
在casa的Mediator
中,認為不需要注冊.因為注冊實際上是一種映射
,而有了target-action的話,其實只需要一種轉(zhuǎn)換
即可.
然后通過url轉(zhuǎn)換
成target-action進行執(zhí)行:
id result = [self performTarget:url.host action:actionName params:params shouldCacheTarget:NO];
不過在實際中,因為各種原因,url本身是不會附帶這些信息的.比如url是服務端定的,還要考慮到android等,所以實際情況沒有那么理想.
所以才有注冊一說,才有了是否需要注冊的爭論.
放開這些爭論,這里有個Tip是:即使選擇了注冊方案,也無須手動注冊,自動注冊即可
.
因為有objc_getClassList
這個東東,小蝦的公眾號專門聊過.
假設在你的Router中,需要用到注冊這貨(不用就算了).這或許是一個方案:
- 有一組或者一個.h文件專門負責記錄有哪些URL.這一步從程序上來講,是沒用的,但是從維護的角度來講,是有意義的:
static NSString *const SchemeShooseVideo = @"jumeimall://page/choosevideo";
2.有一個protocol,來約定服務提供方提供的服務(UPDATE:現(xiàn)在取消了這個protocol,否則服務方會依賴這個protocol.目前是直接約定supportedSchemes方法,為了防止命名沖突,方法名可以增加前綴):
@protocol JMMallProtocol <NSObject>
@required
+ (NSArray <NSString *> *)supportedSchemes;
@end
因為服務方可能提供多個URL遠程服務,所以是個NSArray
;因為我們使用target-action的方案,所以NSArray
中的是字符串,其中包含了target-action信息,也包含了其他的信息(比如url).
3.因為該服務最終由Mediator
解析成target-action并且執(zhí)行,所以字符串必須按照規(guī)定的方式進行組裝.所以最好提供一個helper方法.
+ (NSString *)urlFromScheme:(NSString *)url target:(NSString *)target isClass:(BOOL)isClass action:(NSString *)action {
return [NSString stringWithFormat:@"%@^%@^%@^%@",url,target,(isClass? @"C" : @"O"),action];
}
4.最后使用objc_getClassList
這貨進行一個處理,當然注意下細節(jié)即可.
+ (void)load {
int classCount;
Class *classes;
classCount = objc_getClassList(NULL, 0);
if (classCount <= 0) {
return;
}
NSMutableDictionary *moduls = @{}.mutableCopy;
classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * classCount);
classCount = objc_getClassList(classes, classCount);
for (int i = 0; i < classCount; i++) {
Class c = classes[i];
const char *name = class_getName(c);
if (strncmp(name, "JM", 2) != 0 && strncmp(name, "SC", 2) != 0) {
continue;
}
SEL selector = NSSelectorFromString(@"supportedSchemes");
if (![c respondsToSelector:selector]) {
continue;
}
Method method = class_getClassMethod(c, selector);
if (strncmp(method_getDescription(method)->types, "@", 1) != 0) {
continue;
}
IMP imp = method_getImplementation(method);
NSArray <NSString *> *result = (NSArray <NSString *> *)imp(c,selector);
for (NSString *url in result) {
NSRange range = [url rangeOfString:@"^"];
if (range.location == NSNotFound) {
continue;
}
moduls[[url substringToIndex:range.location]] = url;
}
}
[[JMMediator sharedInstance] setValue:moduls forKey:@"modules"];
free(classes);
}
主要是處理字符串,所以char *
和NSString
之間的轉(zhuǎn)換是挺耗時的操作,所以能用c方法的盡量用.我這里大概2w多個文件處理完畢,耗時0.1s.如果有需求,當然可以做進一步優(yōu)化:)
通過這樣的處理,就可以有以下效果:
- 有一個/多個列表(.h文件),可以知道需要處理那些url
- 在內(nèi)存中維護了一個字典,key為url,value為我們組合的信息(target-action)
- 通過
Mediator
,在處理remote model的時候,可以通過url查詢字典,拿到對應的字符串,解析后進行target-action的方式進行執(zhí)行.
當然缺點就是..調(diào)用反轉(zhuǎn)了:
應該由調(diào)用方?jīng)Q定哪個url對應執(zhí)行哪個服務,而非服務方將服務進行綁定.
不過一方面是這是個架構藝術的問題,另一方面這也是個tip,如果你要這么做,可以幫著省點事.不這么做,完全可以有其他做法.Up to you!
當然不敢獻丑,具體代碼就不好意思拿出來了.Just a tip!