一.前言
iOS开发更新APP我觉得是比较坑的就是审核时间比较长,审核比较严,对于刚入行的小伙伴来说,雷区比较多;所以热更新是比较重要的;
大家也许会发现我们常用的QQ现在下来也就一百多兆,但是用了几个月后发现QQ在手机上占有一个多G的内存,特别是手机内存比较小的小伙伴,这是因为你在使用过程中,有一些功能是你下载下来的;
二.创建Framework
1.新建项目
新建一个Cocoa Touch Framework项目,然后在这个项目里面写你的新的功能,比如我创建了一个控制器,在控制器里面加载一张图和一个label;
<span style="font-size:18px;">- (void)uiConfig{ self.title = @"这是功能2"; UIImageView *imageView = [[UIImageView alloc]init]; imageView.frame = CGRectMake(0, 0, ScreenWidth, ScreenHeight); NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://img4.duitang.com/uploads/item/201405/31/20140531174207_hH5u4.thumb.700_0.jpeg"]]; imageView.image = [UIImage imageWithData:data]; [self.view addSubview:imageView]; UILabel *label = [[UILabel alloc]init]; label.backgroundColor = [UIColor clearColor]; label.frame = CGRectMake(0, (ScreenHeight - 100)/2, ScreenWidth, 100); label.numberOfLines = 0; label.text = @"这是功能2这是功能2这是功能2这是功能2这是功能2这是功能2这是功能2这是功能2这是功能2这是功能2这是功能2这是功能2这是功能2这是功能2这是功能2"; [self.view addSubview:label]; }</span>
2.添加Aggregate
在TARGETS里面新建一个Aggregate
3.添加Run Script脚本
4.脚本源码
<span style="font-size:18px;"># Sets the target folders and the final framework product. # 如果工程名称和Framework的Target名称不一样的话,要自定义FMKNAME # 例如: FMK_NAME = "MyFramework" FMK_NAME=${PROJECT_NAME} # Install dir will be the final output to the framework. # The following line create it in the root folder of the current project. INSTALL_DIR=${SRCROOT}/Products/${FMK_NAME}.framework # Working dir will be deleted after the framework creation. WRK_DIR=build DEVICE_DIR=${WRK_DIR}/Release-iphoneos/${FMK_NAME}.framework SIMULATOR_DIR=${WRK_DIR}/Release-iphonesimulator/${FMK_NAME}.framework # -configuration ${CONFIGURATION} # Clean and Building both architectures. xcodebuild -configuration "Release" -target "${FMK_NAME}" -sdk iphoneos clean build xcodebuild -configuration "Release" -target "${FMK_NAME}" -sdk iphonesimulator clean build # Cleaning the oldest. if [ -d "${INSTALL_DIR}" ] then rm -rf "${INSTALL_DIR}" fi mkdir -p "${INSTALL_DIR}" cp -R "${DEVICE_DIR}/" "${INSTALL_DIR}/" # Uses the Lipo Tool to merge both binary files (i386 + armv6/armv7) into one Universal final product. lipo -create "${DEVICE_DIR}/${FMK_NAME}" "${SIMULATOR_DIR}/${FMK_NAME}" -output "${INSTALL_DIR}/${FMK_NAME}" rm -r "${WRK_DIR}" open "${INSTALL_DIR}" </span>
5.运行打包
运行工程,将生成的framework包压缩zip,然后上传服务器;
例如:http://7xqdun.com1.z0.glb.clouddn.com/FunctionZFJ1.framework.zip
三.创建项目
在项目中我们主要是下载和读取framework包;我们先要获取功能列表,在此我在本地写了一个功能列表,大家如果用得到可以将功能列表存放在服务器上;
1.创建功能列表数据
我添加了四个功能模块,存在NSUserDefaults里面;其中功能1和功能2有下载地址,其他的没有;功能1是个NSObject,功能2直接是一个控制器;
isopen:1表示打开,0表示关闭;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible]; //添加假的功能列表 NSArray *functionList = [USER_DEFAULT objectForKey:@"functionList"]; if(functionList==nil || functionList.count==0){ NSArray *titleArr = @[@"功能1",@"功能2",@"功能3",@"功能4"]; NSArray *className = @[@"HotUpdateControl",@"ZFJViewController",@"",@""]; NSArray *classType = @[@"NSObject",@"UIViewController",@"",@""]; NSArray *downUrl = @[ @"http://7xqdun.com1.z0.glb.clouddn.com/HotMudel.framework.zip", @"http://7xqdun.com1.z0.glb.clouddn.com/FunctionZFJ1.framework.zip", @"", @""]; NSMutableArray *functionArr = [[NSMutableArray alloc]init]; for (int i = 0; i<titleArr.count; i++) { NSMutableDictionary *dict = [[NSMutableDictionary alloc]init]; [dict setObject:titleArr[i] forKey:@"name"]; [dict setObject:className[i] forKey:@"classname"]; [dict setObject:classType[i] forKey:@"classtype"]; [dict setObject:@(i) forKey:@"mid"]; [dict setObject:@"0" forKey:@"isopen"];//0 未开启 1开启了 [dict setObject:downUrl[i] forKey:@"downurl"]; [functionArr addObject:dict]; } [USER_DEFAULT setObject:functionArr forKey:@"functionList"]; [USER_DEFAULT synchronize]; } DynamicViewController *dvc = [[DynamicViewController alloc]init]; UINavigationController *nvc = [[UINavigationController alloc]initWithRootViewController:dvc]; self.window.rootViewController = nvc; return YES; }
2.展示功能列表
在功能列表主要用于展示所有打开过的功能,也就是isopen为1的所有功能;
a.获取本地所有打开的数据,然后在tableview上显示
- (void)getDataBase{ [self.dataArray removeAllObjects]; NSArray *functionList = [USER_DEFAULT objectForKey:@"functionList"]; for (NSDictionary *dict in functionList) { NSInteger isopen = [dict[@"isopen"] integerValue]; if(isopen==1){ [self.dataArray addObject:dict]; } } [self.tableview reloadData]; }
b.点击对于的tableviewcell 的时候跳转对应的framework读取出来的方法
注意,我将不同的framework存放在不同的文件夹下,以mid作为区分;
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ [tableView deselectRowAtIndexPath:indexPath animated:YES]; NSDictionary *dict = self.dataArray[indexPath.row]; //获取framework的路径名,我已mid区分 NSString *destinationPath = [NSHomeDirectory() stringByAppendingString:[NSString stringWithFormat:@"/Documents/FunctionZFJ%@",dict[@"mid"]]]; NSArray* arrFramework = [self getFilenamelistOfType:@"framework" fromDirPath:destinationPath]; NSString *bundlePath = [NSString stringWithFormat:@"%@/%@",destinationPath,[arrFramework lastObject]]; if (![[NSFileManager defaultManager] fileExistsAtPath:bundlePath]) { NSLog(@"文件不存在"); return; } NSBundle *bundle = [NSBundle bundleWithPath:bundlePath]; if (!bundle || ![bundle load]) { NSLog(@"bundle加载出错"); } NSString *className = dict[@"classname"]; NSString *classtype = dict[@"classtype"]; Class loadClass = [bundle classNamed:className]; if (!loadClass) { NSLog(@"获取失败"); return; } if([classtype isEqualToString:@"NSObject"]){ NSObject *bundleObj = [loadClass new]; NSArray *arrVc = [bundleObj performSelector:@selector(getVcs)]; TabController *tvc = [[TabController alloc]initwithVcArray:arrVc]; [self.navigationController pushViewController:tvc animated:YES]; }else if([classtype isEqualToString:@"UIViewController"]){ UIViewController *uvc = (UIViewController *)[loadClass new]; [self.navigationController pushViewController:uvc animated:YES]; } }
c.效果图
3.更多功能
在这里我们可以打开或者关闭某个功能;
a.获取所以功能,包括打开或者关闭状态的;然后在tableview上显示;
<span style="font-size:18px;">#pragma mark - 获取全部数据 - (void)getDataBase{ [self.dataArray removeAllObjects]; NSArray *functionList = [USER_DEFAULT objectForKey:@"functionList"]; NSMutableArray *openYES = [[NSMutableArray alloc]init]; NSMutableArray *openNO = [[NSMutableArray alloc]init]; for (NSDictionary *dict in functionList) { NSMutableDictionary *muDict = [[NSMutableDictionary alloc]initWithDictionary:dict]; NSInteger isopen = [muDict[@"isopen"] integerValue]; if(isopen==1){ //已经打开的功能 [openYES addObject:muDict]; }else{ //没有打开的功能 [openNO addObject:muDict]; } } [self.dataArray addObject:openNO]; [self.dataArray addObject:openYES]; [self.tableview reloadData]; }</span>
b.打开功能
打开某个功能就是下载对应的framework,把下载下来的zip包进行解压一下然后获取到framework,接着删除zip包,把framework放在对于的目录下;最后改变本地列表功能的状态;
<span style="font-size:18px;">#pragma mark - 开启某个功能 先下载数据 - (void)SSZipArchiveDataBaseWithDict:(NSMutableDictionary *)dict{ NSString *requestURL = dict[@"downurl"]; if(requestURL==nil || requestURL.length==0){ self.progresslabel.text = [NSString stringWithFormat:@"%@-没有下载地址,不能开启!",dict[@"name"]]; UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"提示" message:@"没有下载地址,不能开启" preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *sureBtn = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]; [alertController addAction:sureBtn]; [self presentViewController:alertController animated:YES completion:nil]; return; } //下载保存的路径 NSString *savedPath = [NSHomeDirectory() stringByAppendingString:[NSString stringWithFormat:@"/Documents/FunctionZFJ%@.framework.zip",dict[@"mid"]]]; AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer]; NSMutableURLRequest *request = [serializer requestWithMethod:@"POST" URLString:requestURL parameters:nil error:nil]; AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc]initWithRequest:request]; [operation setOutputStream:[NSOutputStream outputStreamToFileAtPath:savedPath append:NO]]; [operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) { float progress = (float)totalBytesRead / totalBytesExpectedToRead; self.progresslabel.text = [NSString stringWithFormat:@"%@下载进度:%.2f",dict[@"name"],progress]; }]; [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { NSLog(@"下载成功"); NSString *destinationPath = [NSHomeDirectory() stringByAppendingString:[NSString stringWithFormat:@"/Documents/FunctionZFJ%@",dict[@"mid"]]]; //对下载下来的ZIP包进行解压 BOOL isScu = [SSZipArchive unzipFileAtPath:savedPath toDestination:destinationPath]; if(isScu){ NSLog(@"解压成功"); NSFileManager *fileMgr = [NSFileManager defaultManager]; BOOL bRet = [fileMgr fileExistsAtPath:savedPath]; if (bRet) { [fileMgr removeItemAtPath:savedPath error:nil];//解压成功后删除压缩包 } [dict setValue:@"1" forKey:@"isopen"]; [self updataBaseWithDict:dict];//解压成功后更新本地功能列表状态 }else{ NSLog(@"解压失败 --- 开启失败"); } } failure:^(AFHTTPRequestOperation *operation, NSError *error) { NSLog(@"下载失败 --- 开启失败"); }]; [operation start]; } </span>
更新本地数据
<span style="font-size:18px;">#pragma mark - 更新本地数据 - (void)updataBaseWithDict:(NSMutableDictionary *)dict{ NSInteger mid = [dict[@"mid"] integerValue]; NSMutableArray *functionList = [USER_DEFAULT objectForKey:@"functionList"]; NSMutableArray *dataArr = [[NSMutableArray alloc]initWithArray:functionList]; [dataArr replaceObjectAtIndex:mid withObject:dict]; [USER_DEFAULT setObject:dataArr forKey:@"functionList"]; BOOL isScu = [USER_DEFAULT synchronize]; if(isScu){ [self getDataBase];//重新获取数据 更新列表 if(self.refreshData){ self.refreshData(); } }else{ NSLog(@"c操作失败"); } } </span>
c.关闭功能
关闭某个功能,也就是删除某个功能的framework,然后更改功能列表的状态;
<span style="font-size:18px;">#pragma mark - 关闭某个功能 - (void)delectFunctionZFJWithDict:(NSMutableDictionary *)dict{ NSFileManager *fileMgr = [NSFileManager defaultManager]; NSString *savedPath = [NSHomeDirectory() stringByAppendingString:[NSString stringWithFormat:@"/Documents/FunctionZFJ%@",dict[@"mid"]]]; BOOL bRet = [fileMgr fileExistsAtPath:savedPath]; if (bRet) { NSError *err; //关闭某个功能 就是删除本地的framework 然后修改本地功能状态 BOOL isScu = [fileMgr removeItemAtPath:savedPath error:&err]; if(isScu){ [dict setValue:@"0" forKey:@"isopen"]; [self updataBaseWithDict:dict]; }else{ NSLog(@"关闭失败"); } }else{ NSLog(@"关闭失败"); } } </span>
d.效果图
四.源代码
在这里面有,两个framework的源代码,可项目的代码;
注意,如果有多个功能的framework,记住多个framework的命名在同一个功能里面不能重复,不然调取失败;
链接: https://pan.baidu.com/s/1mieyk3I 密码: mg4m
五.效果图