在很早的時候,就考慮過換膚功能的實現(xiàn)允扇,一直到現(xiàn)在為止都沒有看到特別好的系統(tǒng)化的實現(xiàn)缠局。所以這里自己實現(xiàn)了一套自認為比較好的DDSkin,同時總結一下幾種實現(xiàn)方式的利弊考润。
實現(xiàn)方式
總的來說實現(xiàn)方式應該是比較統(tǒng)一的狭园,使用string類型的key來代替各個image, color屬性。
最早的時候糊治,考慮過使用Proxy來替換默認的image, color實現(xiàn)唱矛,將消息代理到真正的實例,這樣就可以動態(tài)的替換底層映射的真實對象了井辜。但是這樣做有一個問題绎谦,真實對象變化后無法主動更新到界面,這個比較難以觸發(fā)自動更新粥脚,所以不太靠譜窃肠。
手動模式
在更換皮膚的時候發(fā)出通知,在各個需要變化皮膚的地方手動注冊通知刷允,并且更新UI冤留。
這是最笨的方法,但是在少量場景的時候也是最好的解決方法树灶,簡單而且侵入性小搀菩。
method swizzling
既然我們覺得注冊通知并更新UI這種操作比較固定,而且繁瑣破托,可以一次性的解決肪跋,那么很容易想到去hook部分接口,自動注冊監(jiān)聽土砂。
雖然這樣解決了通知注冊的問題州既,但是method swizzling本身就不是一種好的解決方案谜洽。
- hook的方法是否可以被繞過,通過不同方式創(chuàng)建的對象所調(diào)用的方法是不一樣的吴叶。
- 每個對象都會參與監(jiān)聽阐虚,會導致監(jiān)聽對象非常龐大,并且可能不需要更新的對象也會加入監(jiān)聽蚌卤。
- 侵入性大实束,我們只能hook一些基類的方法,一不小心可能就會注冊兩次逊彭。
associated object
同樣作為通知的方案咸灿,既然method swizzling不行,那么可以讓一個第三方對象去監(jiān)聽侮叮,然后自動觸發(fā)更新避矢。
這是一個比較好的解決方案,他減少了侵入性囊榜,并且更加靈活以及可靠审胸。但是同樣,作為一個修改基類來實現(xiàn)的方案也有很多的缺點卸勺。
- 由于associated object綁定實在基類進行砂沛,那么就不能排除子類覆蓋了該方法的可能性,同時選定那個基類也是個問題曙求,NSObject, UIView?
- 同時侵入性雖然小了碍庵,但還是存在的,畢竟影響的是基類的行為圆到。
- 使用了objc runtime怎抛,這意味著什么呢卑吭?在一個swift為趨勢的環(huán)境下芽淡,這種方案也是一種一般的解決方案。
weak table
參考weak屬性的實現(xiàn)方式豆赏,這里可以使用weak table挣菲。將所有有換膚需求的對象注冊到一個weak table中,在換膚的時候只需要更新表中的對象即可掷邦,這樣就不需要通知白胀,同時也分離了換膚這個功能和實際對象之間的聯(lián)系。
特性
既然我們是一個通用型的框架抚岗,就必須考慮幾點。
通用性
既然我們支持了UIView的屬性,那么可能我們會需要支持非視圖的屬性弥鹦,比如View model,那么考慮到如此的通用性认境,設計的時候就不能局限于View。
同時挟鸠,對于swift對象也可以比較好的支持叉信。
擴展性
有很多樣式,不是簡單的配置屬性就能夠達到效果的艘希,比如富文本等硼身,那么就要求框架能夠有一定的擴展性。
DDSkin
簡介
主要分成3部分
- core 負責注冊對象覆享,并且在樣式更新時觸發(fā)所有注冊對象的更新佳遂。
- handler 對象更新操作,負責具體的更新操作淹真。
- storage 皮膚樣式存儲讶迁,可支持繼承。
core
使用了讀寫鎖來確保線程安全核蘸,實際使用時由于UI操作需要在主線程巍糯,所以基本上來說都會在主線程操作,這里的鎖可能會有點多余客扎。
提供了c和oc兩種接口祟峦,使用c是為了減少消息調(diào)用開銷,實際情況應該也不會有太大影響徙鱼。
// 注冊配置項
void DDSkinRegisterTargetHandler(NSObject *target, DDSkinHandler *handler, BOOL apply) {
NSCParameterAssert(target != nil);
NSMapTable<NSObject *, NSMutableSet<DDSkinHandler *> *> *mapTable = DDSkinGetTargetHandlerTable();
DDSkinTargetHandlerTableWriteLock({
NSMutableSet<DDSkinHandler *> *handlerSet = [mapTable objectForKey:target];
if (handlerSet == nil) {
handlerSet = [[NSMutableSet alloc] init];
[mapTable setObject:handlerSet forKey:target];
}
[handlerSet addObject:handler];
});
if (apply) {
// When apply is true, must call at main thread?
// Usually apply is on the UI thread.
// So we make it must be on the main thread!
DDCAssertMainThread();
DDMainThreadRun({
[handler handleSkinChanged:DDSkinGetCurrentStorage() target:target];
});
}
}
// 更新配置
void DDSkinRefreshAllTarget() {
NSMapTable<NSObject *, NSMutableSet<DDSkinHandler *> *> *mapTable = DDSkinGetTargetHandlerTable();
DDMainThreadRun({
[[NSNotificationCenter defaultCenter] postNotificationName:DDSkinStorageWillChangeNotification object:nil];
DDSkinTargetHandlerTableReadLock({
for (NSObject *target in mapTable.keyEnumerator) {
NSMutableSet<DDSkinHandler *> *handlerSet = [mapTable objectForKey:target];
for (DDSkinHandler *handler in handlerSet) {
[handler handleSkinChanged:DDSkinGetCurrentStorage() target:target];
}
}
});
[[NSNotificationCenter defaultCenter] postNotificationName:NSCurrentLocaleDidChangeNotification object:nil];
});
}
handler
為了保證通用性和可擴展性宅楞,這里默認提供了兩種實現(xiàn)。keyPath和block袱吆。keyPath使用的是setValue接口厌衙,屬于上層接口,并不涉及oc底層绞绒,所以可以支持swift原生類婶希。同時block提供了一種更為靈活的方案。
storage
本身不同團隊會有不同的數(shù)據(jù)存儲方案蓬衡,那么這一塊的變動應該是框架里最大的喻杈,所以這里提供的是協(xié)議,并且默認實現(xiàn)了一套以NSDictionary為基礎的的方案狰晚。
@protocol DDSkinStorageProtocol <NSObject>
- (NSObject *)objectForKey:(NSString *)key;
- (UIColor *)colorForKey:(NSString *)key;
- (NSString *)stringForKey:(NSString *)key;
- (NSURL *)urlForKey:(NSString *)key;
- (UIImage *)imageForKey:(NSString *)key;
- (NSNumber *)numberForKey:(NSString *)key;
- (UIFont *)fontForKey:(NSString *)key;
- (NSNumber *)booleanForKey:(NSString *)key;
- (NSValue *)sizeForKey:(NSString *)key;
@end
每種類型設計一個接口是為了確保類型安全筒饰,防止因為誤操作而出現(xiàn)的類型錯誤。
關于image壁晒,如果我們每次解析完都保存為UIImage對象瓷们,會導致內(nèi)存的浪費,所以這里提供一種lazy-load的方案。這是具體實現(xiàn)上的方案谬晕,完全可以自己實現(xiàn)式镐。
@protocol DDSkinStorageItemLazyLoad <NSObject>
- (id)value;
@end
UI層擴展
基于以上幾點,那么UI層就不需要在基類中做什么事情了固蚤,只需要在支持的類型上增加部分擴展方法即可娘汞。
@property (strong, nonatomic, nullable) IBInspectable NSString *backgroundColorSkinKey;
- (NSString *)backgroundColorSkinKey {
DDSkinHandler *handler = DDSkinGetTargetHandlerByKey(self, DDSelStr(backgroundColor));
return handler.storageKey;
}
- (void)setBackgroundColorSkinKey:(NSString *)backgroundColorSkinKey {
DDAssertMainThread();
if (backgroundColorSkinKey) {
DDSkinHandler *handler = [DDSkinHandler handlerWithKeyPath:DDSelStr(backgroundColor)
valueType:DDSkinHandlerKeyPathValueTypeColor
storageKey:backgroundColorSkinKey];
DDSkinRegisterTargetHandler(self, handler, true);
}
else {
DDSkinUnregisterTargetHandler(self, DDSelStr(backgroundColor));
self.backgroundColor = nil;
}
}
由于大部分場景下這部分代碼是重復的,所以這里使用了大量宏定義來解決這個問題夕玩。
// 上述內(nèi)容可以改為
DDSkinPropertyDefine(backgroundColor, BackgroundColor, color, Color);
xcode高亮狀態(tài):
為什么把key定義成這樣你弦,不加前綴是為了在IB中設置的時候不會每個都有個奇怪的前綴。
使用
如果使用的是IB或者StoryBoard燎孟,可以直接設置屬性一樣配置
如果使用代碼編寫也只需要更新屬性
[self.view setBackgroundColorSkinKey:@"red"];
self.view.backgroundColorSkinKey = @"red";
storage的默認實現(xiàn)為DDSkinDefaultStorageParser
禽作,也可以自定義實現(xiàn)。默認配置文件實現(xiàn)為plist揩页,支持繼承旷偿,super為父配置。
總結
可以看到爆侣,雖然DDSkin的出發(fā)點是一套換膚方案萍程,但實際上來說概念應該更加的廣,應該定義為一套配置化方案兔仰。由于其他配置化的數(shù)據(jù)刷新可能不像UI那么簡單茫负,autolayout可以自動更新,使用上會稍顯麻煩一點乎赴。