动态配置Tabbar图标最优解
最近需要实现动态配置TabbarIcon的需求,主要是针对节假日活动时,由后台配置富有节日元素的图标,我们移动端来替换展示。乍看这个需求很简单,就是从后台读取数据,拿到图片url后替换tabbar图标资源。可是在做的过程中发现还是需要思考一些东西的。下面我们一步步来实现。
实现思路
-
黄铜玩家思路
黄铜玩家自然指的的是新手,针对于初级开发工程师,他们拿到这个需求后,一般不会考虑太多,就是干。他们可能会在app启动后的首页调用配置tabbar图标的接口,获取后直接赋值给
UITabBarItem的image和selectedImage。如果后台接口没返回就使用默认图标。伪代码实现可能是这样的:
#pragma mark - 网络请求
- (void)configTabBarIcons{
if(success){//请求成功
NSMutableArray *imagesUrlArr;//网络请求获取的icon的url数组
if(imagesUrlArr.count){ //判断图片url数组是否为空
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]];
UIImage *image = [UIImage imageWithData:data];
NSMutableArray *imagesArr;//存放image的数组
for (int i = 0; i < itemArray.count; i ++) {//根据item个数遍历替换图片资源
UITabBarItem * item = itemArray[i];
item.image=[[UIImage imageNamed:imagesArr[i]] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
}else{//未获取到图片url数组
//现实默认icon
for (int i = 0; i < itemArray.count; i ++) {//根据item个数遍历替换图片资源
UITabBarItem * item = itemArray[i];
item.image=[[UIImage imageNamed:defImageArr[i]] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
}
}
}else{//接口调用失败
Toast(fail);
}
}
这个伪代码很丑我真的不想再写了。很明显他根本没有过多考虑多种可能性,比如接口调用时机,调用失败情况,代码性能就更不用提了。我下面就针对上面伪代码进行一次优化。
-
黄金玩家思路
处于这个阶段的开发者接到这个需求后可能会分几步考虑实现步骤:
(1)接口的调用时机 :动态配置tabbar的图标属于应用层的配置,不应该和普通界面接口一个优先级,通常应该在程序启动时进行配置。可以放在AppDelegate里的 didFinishLaunchingWithOptions:方法内。
(2)接口的调用失败处理 :正常调试没问题后,不应该认为此功能开发完毕。需要多考虑下其他非正常情况,比如接口调用失败。此时就不应该像 黄铜玩家那样,直接toast提示失败信息。因为此接口属于应用级别不同于界面接口。我们不应该告诉用户获取配置icon失败等错误信息,用户也不明白这是什么意思。而且要考虑获取接口失败或者获取不到配置icon的数据是需要设置默认的tabbar图标。绝不简单的toast提示。
(3)性能问题 : 这个阶段开发者都应该具备了代码性能的考虑。针对这个需求,是不是启动应用都需要拉取接口,获取到数据后是不是每次都要通过 dataWithContentsOfURL:方法把Url转为NSData处理,并生成UIImage对象。此时可能会考虑到缓存。思路是在获取到UIImage对象后,写入沙盒 [UIImagePNGRepresentation(image) writeToFile:fullPath atomically:YES]。在需要的时候从沙盒中读取数据 [UIImage imageWithContentsOfFile:fullPath]。期间会发现图片显示问题,查阅资料后通过,图片命名以@2x结尾后存入本地,默认是@1x的。这样以来,每次都先判断沙盒内是否缓存的有对应的图标,有的直接取出使用,没的话再生新的UIImage对象并存入沙盒内。
这样以来看似好了很多,但深入考虑后还是有改进的地方。 -
钻石玩家思路
姑且称作钻石玩家吧,王者玩家本人还没达到那种意识。需要优化的地方主要在 性能问题 :
一, 缓存 :上面提到了缓存。但做的还不够。这个阶段的高级开发者,首先会综合考虑 dataWithContentsOfURL:这个方法的性能怎样,他是同步还是异步的。处理小资源文件效率怎样。是否需要使用多线程,GCD,NSOperation,去处理。我综合考虑后认为处理icon这些小的图片资源影响不是很大,你当然可以使用更高级的多线程去异步下载图片资源,成功后统一缓存。我这里就不涉及了。在我决定使用 dataWithContentsOfURL:后。我会重新考虑缓存问题。如果每次有新的资源进来就缓存,那之前旧的缓存icon已经没用了却还站着内存。有的人说直接缓存到一定大小后,统一清除缓存。那么我要说,缓存多少后清除,清除时机又要放在哪里合适。就是这俩个问题都搞定了,这也不是最优方案,在清除缓存前,没用的icon仍然占用着内存。我是这样想的,由于此需求是真对tabbarItem的,我们是确定有几个item的,假如是四个,那么需要缓存的图片就是8个 选中和未选择两种状态的icon。再有新的icon需要替换时,我们只需获取到缓存icon文件下的子文件个数,如果为8个,就把这8个旧的icon给清除了,重新缓存新的icon。这样以来就完美了。没用的缓存icon会第一时间被清除。说到这里缓存是说完了。
二,替换时机 :指的是拉取到配置图标后何时替换,是获取一个就替换还是8个都获取完成了在替换。这就涉及到两个问题。首先就算你拉取接口成功,返回给你了8个图标url,但也可能在调用 dataWithContentsOfURL:失败返回nil对象。url可能不合法,当然这是后台数据问题,但我们也要兼容考虑。我们可以这样处理,由于dataWithContentsOfURL:失败返回nil,我们可以在获取到图标资源url地址后,通过遍历,如果8个图标url都能成功生成UIImage对象,就代表此时可对tabbar图标替换,只要有任何一个返回nil就不替换使用默认图标。以免造成有的item的图标是旧的有的是新的不统一的bug。
下面把主要代码贴出:
/**
网络图片存入沙盒并返回(针对tabBarIcon使用)
@param imageUrl 网络图片地址
@return 沙盒图片返回
*/
+ (UIImage *)tabBarItemImageUrl:(NSString *)imageUrl
{
NSArray *stringArr = [imageUrl componentsSeparatedByString:@"/"];
NSString *imageName = stringArr.lastObject;
NSString *name = [[imageName componentsSeparatedByString:@"."] firstObject];
imageName = [NSString stringWithFormat:@"%@@2x.png",name];
NSString *path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES) lastObject];
NSLog(@"%@",path);
NSString *iconFilePath = [path stringByAppendingPathComponent:@"tabbarIcon"];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *fullPath = [iconFilePath stringByAppendingPathComponent:imageName];
// 判断文件是否已存在,存在直接读取
if ([[NSFileManager defaultManager] fileExistsAtPath:fullPath]) {
NSLog(@"存在了");
return [UIImage imageWithContentsOfFile:fullPath];
}
//获取iconFilePath下文件个数
NSArray *subPathArr = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:iconFilePath error:nil];
//超过8个说明icon需要更换清除iconFilePath文件
if (subPathArr.count >= 8) {
[fileManager removeItemAtPath:iconFilePath error:nil];
}
//再从新创建iconFilePath文件
BOOL isDir = NO;
// fileExistsAtPath 判断一个文件或目录是否有效,isDirectory判断是否一个目录
BOOL existed = [fileManager fileExistsAtPath:iconFilePath isDirectory:&isDir];
if ( !(isDir == YES && existed == YES) ) {
// 在 Caches 目录下创建一个 tabbarIcon 目录
[fileManager createDirectoryAtPath:iconFilePath withIntermediateDirectories:YES attributes:nil error:nil];
}
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]];
UIImage *image = [UIImage imageWithData:data];
// 将image写入沙河
if ( [UIImagePNGRepresentation(image) writeToFile:fullPath atomically:YES]) {
return [UIImage imageWithContentsOfFile:fullPath];
}else
{
return nil;
}
}
使用时直接调用:
item.image=[[UIImage tabBarItemImageUrl:self.imagesUrlArr[i]] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
好了,以上是本人的拙见,如有不足之处还请指出。