前言
换肤功能是在APP开发过程中遇到的比较多的场景,为了提供更好的用户体验,许多APP会为用户提供切换主题的功能。主题颜色管理涉及到的的步骤有
- 颜色配置
- 使用颜色
- UI元素动态变更的能力
- 动态修改配置
- 主题包管理
- 如何实施
- 优化
效果如下:
DEMO代码:https://gitee.com/dhar/iosdemos/tree/master/YTThemeManagerDemo
颜色配置
因为涉及到多种配置,所以以代码的方式定义颜色实践和维护的难度是比较高的,一种合适的方案是--颜色的配置是通过配置文件的形式进行导入的。配置文件会经过转换步骤,最终形成代码层级的配置,以全局的方式提供给各个模块使用,这里会涉及到一个颜色管理者的概念,一般地这回事一个单例对象,提供全局访问的接口。同一个APP中在不同的模块中保存不同的主题颜色配置,在不同的层级中也可以存在不同的主题颜色配置,因为涉及到层级间的配置差异,所以颜色的配置需要引入一个等级的概念,一般地较高层级颜色的配置等级是高于较低层级的,存在相同的配置较高层级的配置会覆盖较低层级的配置。
我们采用的颜色配置的文件形如下面所示,为什么是在一个json文件的colorkey下面呢,是为了考虑到未来的扩展性,如果不同的主题会涉及到一些尺寸值的差异化,我们可以添加dimensionskey进行扩展配置。
{ "color": { "Black_A":"323232", "Black_AT":"323232", "Black_B":"888888", "Black_BT":"888888", "White_A":"ffffff", "White_AT":"ffffff", "White_AN":"ffffff", "Red_A":"ff87a0", "Red_AT":"ff87a0", "Red_B":"ff5073", "Red_BT":"ff5073", "Colour_A":"377ce4", "Colour_B":"6aaafa", "Colour_C":"ff8c55", "Colour_D":"ffa200", "Colour_E":"c4a27a", } }
有了以上的配置,颜色配置的工作主要就是解析该配置文件,把配置保存在一个单例对象中即可,这部分主要的步骤如下:
- 配置文件类表根据等级排序
- 获取每个配置文件中的配置,进行保存
- 通知外部主题颜色配置发生改变
对应的代码如下,这里有个需要注意的地方是,加载配置文件的时候使用了文件读写锁进行读写的锁定操作,防止读脏数据的发生,直到配置文件加载完成,释放读写锁,这时读进程可以继续。
- (void)loadConfigWithFileName:(NSString *)fileName level:(NSInteger)level { if (fileName.length == 0) { return; } pthread_rwlock_wrlock(&_rwlock); __block BOOL finded = NO; [self.configFileQueue enumerateObjectsUsingBlock:^(YTThemeConfigFile *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { if ([obj.fileName isEqualToString:fileName]) { finded = YES; *stop = YES; } }]; if (!finded) { // 新增配置文件 YTThemeConfigFile *file = [[YTThemeConfigFile alloc] init]; file.fileName = fileName; file.level = level; [self.configFileQueue addObject:file]; // 优先级排序 [self.configFileQueue sortUsingComparator:^NSComparisonResult(YTThemeConfigFile *_Nonnull obj1, YTThemeConfigFile *_Nonnull obj2) { if (obj1.level > obj2.level) { return NSOrderedDescending; } return NSOrderedAscending; }]; [self setupConfigFilesContainDefault:YES]; } pthread_rwlock_unlock(&_rwlock); } - (void)setupConfigFilesContainDefault:(BOOL)containDefault { NSMutableDictionary *defaultColorDict = nil, *currentColorDict = nil; // 加载默认配置 if (containDefault) { defaultColorDict = [NSMutableDictionary dictionary]; [self loadConfigDataWithColorMap:defaultColorDict valueMap:nil isDefault:YES]; self.defaultColorMap = defaultColorDict; } // 加载主题配置 if (_themePath.length > 0) { currentColorDict = [NSMutableDictionary dictionary]; [self loadConfigDataWithColorMap:currentColorDict valueMap:nil isDefault:NO]; self.currentColorMap = currentColorDict; } // 发送主体颜色变更通知 [self notifyThemeDidChange]; } - (void)notifyThemeDidChange { NSArray *allActionObjects = self.actionMap.objectEnumerator.allObjects; for (YTThemeAction *action in allActionObjects) { [action notifyThemeDidChange]; } } - (void)loadConfigDataWithColorMap:(NSMutableDictionary *)colorMap valueMap:(NSMutableDictionary *)valueMap isDefault:(BOOL)isDefault { // 每一次新增一个配置文件,所有配置文件都得重新计算一次,这里有很多重复多余的工作 [self.configFileQueue enumerateObjectsUsingBlock:^(YTThemeConfigFile *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { NSDictionary *dict = nil; if (isDefault) { dict = obj.defaultDict; } else { dict = obj.currentDict; } if (dict.count > 0) { [self loadThemeColorTo:colorMap from:dict]; // 将所有配置表中的color字段的数据都放到colorMap中 } }]; } - (void)loadThemeColorTo:(NSMutableDictionary *)dictionary from:(NSDictionary *)from { NSDictionary<NSString *, NSString *> *colors = from[@"color"]; [colors enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, NSString *_Nonnull obj, BOOL *_Nonnull stop) { // 十六进制字符串转为UIColor UIColor *color = [UIColor yt_nullcolorWithHexString:obj]; if (color) { [dictionary setObject:color forKey:key]; } else { [dictionary setObject:obj forKey:key]; } }]; }
管理者处理处理配置之外,还需要暴露外部接口给客户端使用,以用于获取不同主题下对应的颜色色值、图片资源、尺寸信息等和主题相关的信息。比如我们会提供一个colorForKey方法获取不同主题下的同一个key对应的颜色色值,获取色值的大致步骤如下:
- 从当前的主题配置中获取
- 从默认的主题配置中获取
- 从预留的主题配置中获取
- 如果重定向的配置,递归处理
- 以上步骤都完成还未找到返回默认黑色
这里使用了读写锁的写锁,如果同时有写操作获取了该锁,读取进程会阻塞直到写操作的完成释放锁。
/** 获取颜色值 */ - (UIColor *)colorForKey:(NSString *)key { pthread_rwlock_rdlock(&_rwlock); UIColor *color = [self colorForKey:key isReserveKey:NO redirectCount:0]; pthread_rwlock_unlock(&_rwlock); return color; } - (UIColor *)colorForKey:(NSString *)key isReserveKey:(BOOL)isReserveKey redirectCount:(NSInteger)redirectCount { if (key == nil) { return nil; } ///正常获取色值 id colorObj = [_currentColorMap objectForKey:key]; if (colorObj == nil) { colorObj = [_defaultColorMap objectForKey:key]; } if (isReserveKey && colorObj == nil) { return nil; } ///看看是否有替补key if (colorObj == nil) { NSString *reserveKey = [_reserveKeyMap objectForKey:key]; if (reserveKey) { colorObj = [self colorForKey:reserveKey isReserveKey:YES redirectCount:redirectCount]; } } ///查看当前key 能否转成 color if (colorObj == nil) { colorObj = [UIColor yt_colorWithHexString:key]; } if ([colorObj isKindOfClass:[UIColor class]]) { ///如果是 重定向 或者 替补 key 的color 要设置到 当前 colorDict 里面 // 重定向的配置形如:"Red_A":"Red_B", if (redirectCount > 0 || isReserveKey) { [_currentColorMap ?: _defaultColorMap setObject:colorObj forKey:key]; } return colorObj; } else { if (redirectCount < 3) { // 重定向递归 return [self colorForKey:colorObj isReserveKey:NO redirectCount:redirectCount + 1]; } else { return [UIColor blackColor]; } } }
使用颜色
颜色的使用也是经由管理者的,为了方便,定义一个颜色宏提供给客户端使用
#define YTThemeColor(key) ([[YTThemeManager sharedInstance] colorForKey:key])
客户端使用的代码如下:
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 20, 200, 40)]; label.text = @"Text"; label.textColor = YTThemeColor(kCK_Red_A); label.backgroundColor = YTThemeColor(kCK_Black_H); [self.view addSubview:label];
另外,因为颜色配置的key为字符串类型,直接使用字符串常量并不是个好办法,所以把对应的字符串转换为宏定义是一个相对好的办法。第一个是方便使用,可以使用代码提示;第二个是不容易出错,特别是长的字符串;第三个也会一定程度上的提高效率。
YTColorDefine类的宏定义
// .h 中的声明 ///Black FOUNDATION_EXTERN NSString *kCK_Black_A; FOUNDATION_EXTERN NSString *kCK_Black_AT; FOUNDATION_EXTERN NSString *kCK_Black_B; FOUNDATION_EXTERN NSString *kCK_Black_BT; // .m 中的定义 NSString *kCK_Black_A = @"Black_A"; NSString *kCK_Black_AT = @"Black_AT"; NSString *kCK_Black_B = @"Black_B"; NSString *kCK_Black_BT = @"Black_BT";
主题包管理
在实际的落地项目中,主题包管理涉及到的事项包括主题包下载和解压和动态加载主题包等内容,最后的一步是更换主题配置文件所在的配置路径,为了演示的方便,我们会把不同主题的资源放置在bundle中某一个特定的文件夹下,通过切换管理者中的主题路径配置来达到切换主题的效果,和动态下载更换主题的步骤是一样的。
管理者提供一个设置主题配置的配置路径的方法,在该方法中改变配置路径的同时,重新加载配置即可,代码如下
/** 设置主题文件的路径 @param themePath 文件的路径 */ - (void)setupThemePath:(NSString *)themePath { pthread_rwlock_wrlock(&_rwlock); _themePath = [themePath copy]; self.currentColorMap = nil; if ([_themePath.lowercaseString isEqualToString:[[NSBundle mainBundle] resourcePath].lowercaseString]) { _themePath = nil; } self.currentThemePath = _themePath; for (int i = 0; i < self.configFileQueue.count; i++) { YTThemeConfigFile *obj = [self.configFileQueue objectAtIndex:i]; [obj resetCurrentDict]; } [self setupConfigFilesContainDefault:NO]; pthread_rwlock_unlock(&_rwlock); }
如何实施
以上的流程涉及到的只是iOS平台下的一个技术解决方案,真实的实践过程中会涉及到安卓平台、Web页面、UI出图的标注,这些是要进行统一处理的,才能在各个端上有一致的体验。第一步就是制定合理的颜色规范,把规范同步给各个端的利益相关人员;第二部是UI出图颜色是规范的颜色定义值,而不是比如#ffffff这样的颜色,需要是比如White_A这样规范的颜色定义值,这样客户端处理使用的就是White_A这个值,不用管在不同主题下不同的颜色表现形式。
优化
loadConfigDataWithColorMap方法调用的优化
如果模块很多,每个模块都会调用loadConfigWithFileName加载配置文件,那么loadConfigDataWithColorMap方法处理文件的时间复杂度是O(N*N),会重复处理很多多余的工作,理想的做法是底层保存一份公有的颜色配置,然后在APP层加载一份定制化的配置,在模块中不用再加载主题配置文件,这样会提高效率。
附:读写锁pthread_rwlock_t的使用
读写锁是用来解决读者写者问题的,读操作可以共享,写操作是排他的,读可以有多个在读,写只有唯一个在写,同时写的时候不允许读。
具有强读者同步和强写者同步两种形式
强读者同步:当写者没有进行写操作,读者就可以访问;
强写者同步:当所有写者都写完之后,才能进行读操作,读者需要最新的信息,一些事实性较高的系统可能会用到该所,比如定票之类的。
读写锁的操作:
读写锁的初始化:
定义读写锁: pthread_rwlock_t m_rw_lock;
函数原型: pthread_rwlock_init(pthread_rwlock_t * ,pthread_rwattr_t *);
返回值:0,表示成功,非0为一错误码
读写锁的销毁:
函数原型: pthread_rwlock_destroy(pthread_rwlock_t* );
返回值:0,表示成功,非0表示错误码
获取读写锁的读锁操作:分为阻塞式获取和非阻塞式获取,如果读写锁由一个写者持有,则读线程会阻塞直至写入者释放读写锁。
阻塞式:
函数原型:pthread_rwlock_rdlock(pthread_rwlock_t*);
非阻塞式:
函数原型:pthread_rwlock_tryrdlock(pthread_rwlock_t*);
返回值: 0,表示成功,非0表示错误码,非阻塞会返回ebusy而不会让线程等待
获取读写锁的写锁操作:分为阻塞和非阻塞,如果对应的读写锁被其它写者持有,或者读写锁被读者持有,该线程都会阻塞等待。
阻塞式:
函数原型:pthread_rwlock_wrlock(pthread_rwlock_t*);
非阻塞式:
函数原型:pthread_rwlock_trywrlock(pthread_rwlock_t*);
返回值: 0,表示成功
释放读写锁:
函数原型:pthread_rwlock_unlock(pthread_rwlock_t*);
总结(转):
互斥锁与读写锁的区别:
当访问临界区资源时(访问的含义包括所有的操作:读和写),需要上互斥锁;
当对数据(互斥锁中的临界区资源)进行读取时,需要上读取锁,当对数据进行写入时,需要上写入锁。
读写锁的优点:
对于读数据比修改数据频繁的应用,用读写锁代替互斥锁可以提高效率。因为使用互斥锁时,即使是读出数据(相当于操作临界区资源)都要上互斥锁,而采用读写锁,则可以在任一时刻允许多个读出者存在,提高了更高的并发度,同时在某个写入者修改数据期间保护该数据,以免任何其它读出者或写入者的干扰。
读写锁描述:
获取一个读写锁用于读称为共享锁,获取一个读写锁用于写称为独占锁,因此这种对于某个给定资源的共享访问也称为共享-独占上锁。
有关这种类型问题(多个读出者和一个写入者)的其它说法有读出者与写入者问题以及多读出者-单写入者锁。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对自由互联的支持。