DDSkin做更好的換膚框架

在很早的時候,就考慮過換膚功能的實現(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本身就不是一種好的解決方案谜洽。

  1. hook的方法是否可以被繞過,通過不同方式創(chuàng)建的對象所調(diào)用的方法是不一樣的吴叶。
  2. 每個對象都會參與監(jiān)聽阐虚,會導致監(jiān)聽對象非常龐大,并且可能不需要更新的對象也會加入監(jiān)聽蚌卤。
  3. 侵入性大实束,我們只能hook一些基類的方法,一不小心可能就會注冊兩次逊彭。

associated object

同樣作為通知的方案咸灿,既然method swizzling不行,那么可以讓一個第三方對象去監(jiān)聽侮叮,然后自動觸發(fā)更新避矢。

這是一個比較好的解決方案,他減少了侵入性囊榜,并且更加靈活以及可靠审胸。但是同樣,作為一個修改基類來實現(xiàn)的方案也有很多的缺點卸勺。

  1. 由于associated object綁定實在基類進行砂沛,那么就不能排除子類覆蓋了該方法的可能性,同時選定那個基類也是個問題曙求,NSObject, UIView?
  2. 同時侵入性雖然小了碍庵,但還是存在的,畢竟影響的是基類的行為圆到。
  3. 使用了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):

macros.png

為什么把key定義成這樣你弦,不加前綴是為了在IB中設置的時候不會每個都有個奇怪的前綴。

使用

如果使用的是IB或者StoryBoard燎孟,可以直接設置屬性一樣配置

ib.png

如果使用代碼編寫也只需要更新屬性

[self.view setBackgroundColorSkinKey:@"red"];
self.view.backgroundColorSkinKey = @"red";

storage的默認實現(xiàn)為DDSkinDefaultStorageParser禽作,也可以自定義實現(xiàn)。默認配置文件實現(xiàn)為plist揩页,支持繼承旷偿,super為父配置。

plist.png

總結

可以看到爆侣,雖然DDSkin的出發(fā)點是一套換膚方案萍程,但實際上來說概念應該更加的廣,應該定義為一套配置化方案兔仰。由于其他配置化的數(shù)據(jù)刷新可能不像UI那么簡單茫负,autolayout可以自動更新,使用上會稍顯麻煩一點乎赴。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末忍法,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子榕吼,更是在濱河造成了極大的恐慌饿序,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件羹蚣,死亡現(xiàn)場離奇詭異原探,居然都是意外死亡,警方通過查閱死者的電腦和手機度宦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門踢匣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來告匠,“玉大人戈抄,你說我怎么就攤上這事『笞ǎ” “怎么了划鸽?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我裸诽,道長嫂用,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任丈冬,我火速辦了婚禮嘱函,結果婚禮上,老公的妹妹穿的比我還像新娘埂蕊。我一直安慰自己往弓,他們只是感情好,可當我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布蓄氧。 她就那樣靜靜地躺著函似,像睡著了一般。 火紅的嫁衣襯著肌膚如雪喉童。 梳的紋絲不亂的頭發(fā)上撇寞,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天,我揣著相機與錄音堂氯,去河邊找鬼蔑担。 笑死,一個胖子當著我的面吹牛咽白,可吹牛的內(nèi)容都是我干的钟沛。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼局扶,長吁一口氣:“原來是場噩夢啊……” “哼恨统!你這毒婦竟也來了?” 一聲冷哼從身側響起三妈,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤畜埋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后畴蒲,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體悠鞍,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年模燥,在試婚紗的時候發(fā)現(xiàn)自己被綠了咖祭。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡蔫骂,死狀恐怖么翰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情辽旋,我是刑警寧澤浩嫌,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布檐迟,位于F島的核電站,受9級特大地震影響码耐,放射性物質(zhì)發(fā)生泄漏追迟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一骚腥、第九天 我趴在偏房一處隱蔽的房頂上張望敦间。 院中可真熱鬧,春花似錦束铭、人聲如沸每瞒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽剿骨。三九已至,卻和暖如春埠褪,著一層夾襖步出監(jiān)牢的瞬間浓利,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工钞速, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留贷掖,地道東北人。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓渴语,卻偏偏與公主長得像苹威,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子驾凶,可洞房花燭夜當晚...
    茶點故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容