1、前言:html
Tip: 原本這篇文章在聖誕節就已經準備好了,可是因爲種種緣由一直沒有寫完,今天將它寫出來,也算是2018年的第一篇文章了。你好,2018!ios
過去聖誕節是各大APP濃妝豔抹展示本身衣服的節日,今年的聖誕節彷佛冷清了許多,只看到了幾個APP換膚,那我就從中分析一下吧。git
2、分析:github
我認爲目前的換膚主要分紅3種,一種是返回圖片的地址,APP再根據圖片日誌去取圖片,另外一種是下載zip包而後再解壓去替換圖標,再一種是圖片資源放到包裏,接口控制是否顯示。web
2.1 實現方式一:json
我發現河狸家就是這個方式,爲何先以河狸家來舉例呢?由於朋友說它太炫酷了!因而我就從它開始分析了。緩存
我已經用越獄手機查看了河狸家APP的沙盒,並無發現本地存儲有皮膚文件。sass
因而我開始用Charles進行抓包,我在這個接口發現了疑似皮膚文件的配置信息。網絡
如圖:框架
因而我從img前綴的域名的中發下了請求到的皮膚文件,如圖,這正是tabbar的背景圖片。
值得稱讚的是河狸家的png圖片通過了webp壓縮,這也是目前APP端主流的一個圖片格式。
因此河狸家的方案是接口返回了皮膚的配置信息,配置信息中存有圖片的地址信息,而後經過圖片緩存框架去拿到圖片的。
這種狀況我認爲必定作一下處理,讓全部圖片都緩存完畢後,再顯示,否則可能會出現圖片一個個閃現出來的狀況,甚至於在網絡很差的狀況下,某個圖片顯示不出來的狀況。這個狀況我再另外一個APP上見過,具體哪一個我給忘記了(測試了好多APP,實在記不清了。。。)
2.2 實現方式二:
這裏我以微店買家版進行一個舉例,如圖這是微店買家版聖誕節皮膚。
我一樣是在安裝APP後先看沙盒裏是否有皮膚文件,一樣並無發現。下面直接去抓接口,我在assets的域名上發現了可疑的zip文件包。
如圖:
解壓這個zip文件後,發現了tabbar的圖片資源。我一樣在程序的沙盒裏面發現了一樣的文件。
如圖:
圖片資源拿到了,那麼它們是如何替換的呢?我就以微店買家版進行舉例來看一下。
我拿到微店買家版ipa脫殼後,我分別使用 Hopper Disassembler 和 class-dump 對主程序進行分析。最後發現以下信息:
從中能夠看出它是使用的Category和KVO去實現了替換皮膚的過程。給UIButton等系統類添加一個Category,添加了設置皮膚的方法,經過KVO去實現了觸發控制。
另外這裏建議皮膚下載完成以後能夠去當即觸發換膚,我在測試百度糯米APP的時候發現它是第二次啓動的時候纔去替換,可能由於它是高頻APP吧。
2.3 實現方式三:
這種方式我測試的幾個APP中沒有發現,聽朋友說某註明APP曾經就採用過這個方式。這種方式是在發版前將皮膚文件存儲到包內,經過後臺接口控制去顯示。這種狀況的優勢是便於控制,故障率小。缺點是包的體積過大,而且嚴重依賴於蘋果爸爸的審覈。
3、個人實現方式:
最近我也作了皮膚相關的功能,下面我說一下個人實現思路。
先上圖,看一下個人APP控制邏輯。
個人實現思路相似微店的實現方式。可是我並無使用KVO而是使用了通知註冊的方式。
APP啓動後直接加載對應的皮膚文件,同時另外一個線程去請求後臺皮膚接口,接口返回了一個zip包的連接,下載zip包,解壓後,解析裏面的config.json文件,而後我使用通知的方式去觸發換膚。具體的思路邏輯相信流程圖上已經畫的很清楚了。
控制皮膚是否顯示的邏輯徹底由後臺控制,後臺返回skinSign爲空則關閉皮膚。
下面看一下個人config.json文件的格式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
{
"home_navi"
: {
"colors"
: {
"color_background"
:
"#ffffff"
},
"images"
: {
"image_logo"
:
"home_topLogo"
}
},
"home_tabbar"
: {
"colors"
: {
"color_background"
:
"#F9F9F9"
,
"color_button_normal"
:
"#999999"
,
"color_button_selected"
:
"#444444"
},
"images"
: {
"image_one_button_normal"
:
"tab按鈕1圖片"
,
"image_one_button_selected"
:
"tab按鈕1選中圖片"
,
"image_two_button_normal"
:
"tab按鈕2圖片"
,
"image_two_button_selected"
:
"tab按鈕2選中圖片"
,
"image_three_button_normal"
:
"tab按鈕2圖片"
,
"image_three_button_selected"
:
"tab按鈕2選中圖片"
},
"values"
: {
"value_one_button"
:
"tab按鈕1"
,
"value_two_button"
:
"tab按鈕2"
,
"value_three_button"
:
"tab按鈕3"
}
},
"loading"
: {
"resources"
: {
"resource_refreshImage"
:
"refresh.gif"
}
}
}
|
配置文件中,分爲首頁導航(home_navi)、首頁tabbar(home_tabbar)、加載loading(loading)三個業務模塊。在每一個業務模塊下均可以有4個功能模塊分別是顏色(colors)、圖片(images)、值(values)、資源(resources),這4個模塊根據本身的須要進行添加。colors控制的是顏色,這裏我以16進制值爲準。images控制的是圖片,最普通的png文件。values控制的是值。resources控制的是資源文件,例如json、gif等文件。
我建立了一個UIView的Category,在這個Category中我加了一個方法,以下:
1
|
- (
void
)configSkinMapModule:(NSString *)module skinMap:(NSDictionary *)skinMap;
|
假設我須要給導航欄添加換膚的功能,我只須要加上以下代碼:
1
2
3
4
5
6
7
|
[_tabbarButton configSkinMapModule:kSkin_MODULE_HOME_TABBAR skinMap:
@{kSkinMapKey_button_image : @
"image_one_button_normal"
,
kSkinMapKey_button_selectedImage : @
"image_one_button_selected"
,
kSkinMapKey_button_titleColor : @
"color_button_normal"
,
kSkinMapKey_button_titleSelectedColor : @
"color_button_selected"
,
kSkinMapKey_button_title : @
"value_one_button"
}];
|
我會建立一個SkinConstants文件去定義一下,替換的方式標識。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// button相關
static
NSString *
const
kSkinMapKey_button_image = @
"kSkinMapKey_button_image"
;
static
NSString *
const
kSkinMapKey_button_highlightedImage = @
"kSkinMapKey_button_highlightedImage"
;
static
NSString *
const
kSkinMapKey_button_selectedImage = @
"kSkinMapKey_button_selectedImage"
;
static
NSString *
const
kSkinMapKey_button_disabledImage = @
"kSkinMapKey_button_disabledImage"
;
static
NSString *
const
kSkinMapKey_button_titleColor = @
"kSkinMapKey_button_titleColor"
;
static
NSString *
const
kSkinMapKey_button_titleHighlightedColor = @
"kSkinMapKey_button_titleHighlightedColor"
;
static
NSString *
const
kSkinMapKey_button_titleSelectedColor = @
"kSkinMapKey_button_titleSelectedColor"
;
static
NSString *
const
kSkinMapKey_button_titleDisabledColor = @
"kSkinMapKey_button_titleDisabledColor"
;
static
NSString *
const
kSkinMapKey_button_title = @
"kSkinMapKey_button_title"
;
// label相關
static
NSString *
const
kSkinMapKey_label_text = @
"kSkinMapKey_label_text"
;
static
NSString *
const
kSkinMapKey_label_textColor = @
"kSkinMapKey_label_textColor"
;
static
NSString *
const
kSkinMapKey_label_backgroundColor = @
"kSkinMapKey_label_backgroundColor"
;
// imageview相關
static
NSString *
const
kSkinMapKey_imageView_image = @
"kSkinMapKey_imageView_image"
;
static
NSString *
const
kSkinMapKey_imageView_gif = @
"kSkinMapKey_imageView_gif"
;
// gif動畫
static
NSString *
const
kSkinMapKey_imageView_backgroundColor = @
"kSkinMapKey_imageView_backgroundColor"
;
|
相信從名字大家就能看出來,每個定義都是UIKit裏面的一個方法。
而後我說一下剛纔那個Category中加的方法,其中module對應的正是config.json中的業務模塊,例如home_navi。skinMap中的key是替換的方式標識正是SkinConstants中的定義,value則是config.json中的對應的模塊的key值。
也就是上面加的方法的意思是給這個home_navi業務模塊中的某一個button增長了修改普通模式圖片(kSkinMapKey_button_image)、修改選中模式圖片(kSkinMapKey_button_selectedImage)、普通模式文字顏色(kSkinMapKey_button_titleColor)、修改選中模式圖片(kSkinMapKey_button_selectedImage)、修改文字值(kSkinMapKey_button_title)的功能。
咱們在通知觸發方法中使用以下代碼去執行替換過程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
- (
void
)changeSkin
{
NSDictionary *map = self.skinMap;
if
([self isKindOfClass:[UIButton
class
]]) {
UIButton *obj = (UIButton *)self;
if
(map[kSkinMapKey_button_image]) {
[obj setImage:SkinImage(map[kSkinMapKey_button_image]) forState:UIControlStateNormal];
}
if
(map[kSkinMapKey_button_highlightedImage]) {
[obj setImage:SkinImage(map[kSkinMapKey_button_highlightedImage]) forState:UIControlStateHighlighted];
}
if
(map[kSkinMapKey_button_selectedImage]) {
[obj setImage:SkinImage(map[kSkinMapKey_button_selectedImage]) forState:UIControlStateSelected];
}
if
(map[kSkinMapKey_button_disabledImage]) {
[obj setImage:SkinImage(map[kSkinMapKey_button_disabledImage]) forState:UIControlStateDisabled];
}
if
(map[kSkinMapKey_button_titleColor]) {
[obj setTitleColor:SkinColor(map[kSkinMapKey_button_titleColor]) forState:UIControlStateNormal];
}
...如下省略...
}
|
同時我本地會存有一個localConfig.json用於管理本地的須要替換皮膚的模塊,內容和config.json如出一轍。只是他取的都是本地默認的皮膚資源配置。
SkinImage是處理images模塊的,這個宏定義是pngResourceForSign:方法的宏,用於去處理該加載哪一個圖片文件。
關於colors、resources等其餘模塊我就不一一介紹了,都是大同小異。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 獲取Png資源
- (UIImage *)pngResourceForSign:(NSString *)sign;
{
NSArray *array = [sign componentsSeparatedByString:@
"."
];
NSString *module = array.firstObject;
NSString *key = array.lastObject;
NSDictionary *moduleDic = self.configData[module];
NSDictionary *imageDic = moduleDic[@
"images"
];
NSString *value = imageDic[key];
// 這裏已經在初始化的時候作了判斷,self.path有值則爲後臺皮膚,無值則爲本地默認皮膚。
if
(!self.path.length) {
return
[UIImage imageNamed:value];
}
NSString *filePath = [self.path stringByAppendingFormat:@
"/%@"
,value];
UIImage *image = [UIImage imageWithContentsOfFile:filePath];
return
image;
}
|
上面的例子就是_tabbarButton執行configSkinMapModule:skinMap:方法註冊了一個通知,判斷後臺是否啓用換膚,啓動換膚則加載config.json文件,沒有則加載localConfig.json本地默認皮膚。
以上就是我實現換膚方式的一個思路。
4、總結:
以上各類實現方式都各有各的好處,個人實現方式也有須要優化的地方,例如能夠在後臺接口上加入時間控制,能夠實現提早的緩存方案,而沒必要每次都是在用戶眼皮底下換。若是你有更好的實現方案歡迎一塊兒交流。
參考資料: