iOS實現換膚功能的簡單處理框架(附源碼)

這篇文章主要給大家介紹了關於iOS實現換膚功能的簡單處理框架,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧

前言

換膚功能是在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*);

總結(轉):

互斥鎖與讀寫鎖的區別:

當訪問臨界區資源時(訪問的含義包括所有的操作:讀和寫),需要上互斥鎖;

當對數據(互斥鎖中的臨界區資源)進行讀取時,需要上讀取鎖,當對數據進行寫入時,需要上寫入鎖。

讀寫鎖的優點:

對於讀數據比修改數據頻繁的應用,用讀寫鎖代替互斥鎖可以提高效率。因爲使用互斥鎖時,即使是讀出數據(相當於操作臨界區資源)都要上互斥鎖,而採用讀寫鎖,則可以在任一時刻允許多個讀出者存在,提高了更高的併發度,同時在某個寫入者修改數據期間保護該數據,以免任何其它讀出者或寫入者的干擾。

讀寫鎖描述:

獲取一個讀寫鎖用於讀稱爲共享鎖,獲取一個讀寫鎖用於寫稱爲獨佔鎖,因此這種對於某個給定資源的共享訪問也稱爲共享-獨佔上鎖。

有關這種類型問題(多個讀出者和一個寫入者)的其它說法有讀出者與寫入者問題以及多讀出者-單寫入者鎖。

總結

以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對神馬文庫的支持。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章