66

iOS客户端节日换肤方案探究 | IAN博客

 6 years ago
source link: https://www.ianisme.com/ios/2588.html?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一、前言:

tip: 本来这篇文章在圣诞节就已经准备好了,但是由于种种原因一直没有写完,今天将它写出来,也算是2018年的第一篇文章了。你好,2018!

huanfu0.png

过去圣诞节是各大APP浓妆艳抹展现自己衣服的节日,今年的圣诞节似乎冷清了许多,只看到了几个APP换肤,那我就从中分析一下吧。

二、分析:

我认为目前的换肤主要分成3种,一种是返回图片的地址,APP再根据图片地址去取图片,另一种是下载zip包然后再解压去替换图标,再一种是图片资源放到包里,接口控制是否显示。

2.1 实现方式一:

我发现河狸家就是这个方式,为什么先以河狸家来举例呢?因为朋友说它太炫酷了!于是我就从它开始分析了。

huanfu1.png

我已经用越狱手机查看了河狸家APP的沙盒,并没有发现本地存储有皮肤文件。

于是我开始用Charles进行抓包,我在这个接口发现了疑似皮肤文件的配置信息。

如图:

huanfu2.png


于是我从img前缀的域名的中发下了请求到的皮肤文件,如图,这正是tabbar的背景图片。

huanfu3.png

值得称赞的是河狸家的png图片经过了webp压缩,这也是目前APP端主流的一个图片格式。

所以河狸家的方案是接口返回了皮肤的配置信息,配置信息中存有图片的地址信息,然后通过图片缓存框架去拿到图片的。

这种情况我认为一定做一下处理,让所有图片都缓存完毕后,再显示,不然可能会出现图片一个个闪现出来的情况,甚至于在网络不好的情况下,某个图片显示不出来的情况。这个情况我再另一个APP上见过,具体哪个我给忘记了(测试了好多APP,实在记不清了。。。)

2.2 实现方式二:

这里我以微店买家版进行一个举例,如图这是微店买家版圣诞节皮肤。

huanfu5.png

我同样是在安装APP后先看沙盒里是否有皮肤文件,同样并没有发现。下面直接去抓接口,我在assets的域名上发现了可疑的zip文件包。

如图:

huanfu6.png

解压这个zip文件后,发现了tabbar的图片资源。我同样在程序的沙盒里面发现了同样的文件。

如图:

huanfu7.png

huanfu8.png

huanfu9.png

图片资源拿到了,那么它们是如何替换的呢?我就以微店买家版进行举例来看一下。

我拿到微店买家版ipa脱壳后,我分别使用 Hopper Disassembler 和 class-dump 对主程序进行分析。最后发现如下信息:

huanfu10.png

huanfu11.png

huanfu12.png

huanfu13.png

从中可以看出它是使用的Category和KVO去实现了替换皮肤的过程。给UIButton等系统类添加一个Category,添加了设置皮肤的方法,通过KVO去实现了触发控制。
另外这里建议皮肤下载完成之后可以去立即触发换肤,我在测试百度糯米APP的时候发现它是第二次启动的时候才去替换,可能因为它是高频APP吧。

2.3 实现方式三:

这种方式我测试的几个APP中没有发现,听朋友说某知名APP曾经就采用过这个方式。这种方式是在发版前将皮肤文件存储到包内,通过后台接口控制去显示。这种情况的优点是便于控制,故障率小。缺点是包的体积过大,并且严重依赖于苹果爸爸的审核。

三、我的实现方式:

最近我也做了皮肤相关的功能,下面我说一下我的实现思路。
先上图,看一下我的APP控制逻辑。

huanfu14.png

我的实现思路类似微店的实现方式。但是我并没有使用KVO而是使用了通知注册的方式。
APP启动后直接加载对应的皮肤文件,同时另一个线程去请求后台皮肤接口,接口返回了一个zip包的链接,下载zip包,解压后,解析里面的config.json文件,然后我使用通知的方式去触发换肤。具体的思路逻辑相信流程图上已经画的很清楚了。
控制皮肤是否显示的逻辑完全由后台控制,后台返回skinSign为空则关闭皮肤。

下面看一下我的config.json文件的格式。

{
    "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中我加了一个方法,如下:

- (void)configSkinMapModule:(NSString *)module skinMap:(NSDictionary *)skinMap;

假设我需要给导航栏添加换肤的功能,我只需要加上如下代码:

    [_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文件去定义一下,替换的方式标识。

// 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)的功能。

我们在通知触发方法中使用如下代码去执行替换过程

- (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等其他模块我就不一一介绍了,都是大同小异。

// 获取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本地默认皮肤。
以上就是我实现换肤方式的一个思路。

四、总结:

以上各种实现方式都各有各的好处,我的实现方式也有需要优化的地方,例如可以在后台接口上加入时间控制,可以实现提前的缓存方案,而不必每次都是在用户眼皮底下换。如果你有更好的实现方案欢迎一起交流。

参考资料:
1.github·ThemeManager
2.github·SwiftTheme
3.iOS换肤方案
4.github·EasyTheme
5.「节日换肤」通用技术方案__iOS端实现

如果这篇文章帮助了你,可以请作者喝罐可乐,以此激励作者创作更多!
支持: 微信支付 支付宝

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK