DDSkin做更好的换肤框架

在很早的时候,就考虑过换肤功能的实现,一直到现在为止都没有看到特别好的系统化的实现。所以这里自己实现了一套自认为比较好的DDSkin,同时总结一下几种实现方式的利弊。

实现方式

总的来说实现方式应该是比较统一的,使用string类型的key来代替各个image, color属性。

最早的时候,考虑过使用Proxy来替换默认的image, color实现,将消息代理到真正的实例,这样就可以动态的替换底层映射的真实对象了。但是这样做有一个问题,真实对象变化后无法主动更新到界面,这个比较难以触发自动更新,所以不太靠谱。

手动模式

在更换皮肤的时候发出通知,在各个需要变化皮肤的地方手动注册通知,并且更新UI。

这是最笨的方法,但是在少量场景的时候也是最好的解决方法,简单而且侵入性小。

method swizzling

既然我们觉得注册通知并更新UI这种操作比较固定,而且繁琐,可以一次性的解决,那么很容易想到去hook部分接口,自动注册监听。

虽然这样解决了通知注册的问题,但是method swizzling本身就不是一种好的解决方案。

  1. hook的方法是否可以被绕过,通过不同方式创建的对象所调用的方法是不一样的。
  2. 每个对象都会参与监听,会导致监听对象非常庞大,并且可能不需要更新的对象也会加入监听。
  3. 侵入性大,我们只能hook一些基类的方法,一不小心可能就会注册两次。

associated object

同样作为通知的方案,既然method swizzling不行,那么可以让一个第三方对象去监听,然后自动触发更新。

这是一个比较好的解决方案,他减少了侵入性,并且更加灵活以及可靠。但是同样,作为一个修改基类来实现的方案也有很多的缺点。

  1. 由于associated object绑定实在基类进行,那么就不能排除子类覆盖了该方法的可能性,同时选定那个基类也是个问题,NSObject, UIView?
  2. 同时侵入性虽然小了,但还是存在的,毕竟影响的是基类的行为。
  3. 使用了objc runtime,这意味着什么呢?在一个swift为趋势的环境下,这种方案也是一种一般的解决方案。

weak table

参考weak属性的实现方式,这里可以使用weak table。将所有有换肤需求的对象注册到一个weak table中,在换肤的时候只需要更新表中的对象即可,这样就不需要通知,同时也分离了换肤这个功能和实际对象之间的联系。

特性

既然我们是一个通用型的框架,就必须考虑几点。

通用性

既然我们支持了UIView的属性,那么可能我们会需要支持非视图的属性,比如View model,那么考虑到如此的通用性,设计的时候就不能局限于View。

同时,对于swift对象也可以比较好的支持。

扩展性

有很多样式,不是简单的配置属性就能够达到效果的,比如富文本等,那么就要求框架能够有一定的扩展性。

DDSkin

简介

主要分成3部分

  • core 负责注册对象,并且在样式更新时触发所有注册对象的更新。
  • handler 对象更新操作,负责具体的更新操作。
  • storage 皮肤样式存储,可支持继承。

core

使用了读写锁来确保线程安全,实际使用时由于UI操作需要在主线程,所以基本上来说都会在主线程操作,这里的锁可能会有点多余。

提供了c和oc两种接口,使用c是为了减少消息调用开销,实际情况应该也不会有太大影响。

// 注册配置项
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

为了保证通用性和可扩展性,这里默认提供了两种实现。keyPath和block。keyPath使用的是setValue接口,属于上层接口,并不涉及oc底层,所以可以支持swift原生类。同时block提供了一种更为灵活的方案。

storage

本身不同团队会有不同的数据存储方案,那么这一块的变动应该是框架里最大的,所以这里提供的是协议,并且默认实现了一套以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

每种类型设计一个接口是为了确保类型安全,防止因为误操作而出现的类型错误。

关于image,如果我们每次解析完都保存为UIImage对象,会导致内存的浪费,所以这里提供一种lazy-load的方案。这是具体实现上的方案,完全可以自己实现。

@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;
    }
}

由于大部分场景下这部分代码是重复的,所以这里使用了大量宏定义来解决这个问题。

// 上述内容可以改为
DDSkinPropertyDefine(backgroundColor, BackgroundColor, color, Color);

xcode高亮状态:

macros.png

为什么把key定义成这样,不加前缀是为了在IB中设置的时候不会每个都有个奇怪的前缀。

使用

如果使用的是IB或者StoryBoard,可以直接设置属性一样配置

ib.png

如果使用代码编写也只需要更新属性

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

storage的默认实现为DDSkinDefaultStorageParser,也可以自定义实现。默认配置文件实现为plist,支持继承,super为父配置。

plist.png

总结

可以看到,虽然DDSkin的出发点是一套换肤方案,但实际上来说概念应该更加的广,应该定义为一套配置化方案。由于其他配置化的数据刷新可能不像UI那么简单,autolayout可以自动更新,使用上会稍显麻烦一点。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容