版本更新大致分为四大类:
1.app负责单版本检测测与更新(自己实现版本更新);
2.通过苹果商店的自动更新机制进行自动更新(相当与自己不更新,靠苹果商店自己的自动更新机制更新,这个是苹果提倡的升级机制,但是app几乎都不采用);
3.通过第三方平台实现版本检测与更新(如:友盟的版本检测与更新);
4.app和服务器联合实现多版本检测测与更新(自己实现版本更新)方案。
注意:苹果现在不支持跳转地址含有中文的下载地址了。
像这种使用方法是跳转不到苹果商店了:[[UIApplication sharedApplication] openURL:@"https://apps.apple.com/cn/app/粉猪语音—陪你度过每一个深夜/id1524608462" options:nil completionHandler:nil];。解决办法是把app的地址换成转码后的地址:[[UIApplication sharedApplication] openURL:@"https://apps.apple.com/cn/app/%E7%B2%89%E7%8C%AA%E8%AF%AD%E9%9F%B3-%E9%99%AA%E4%BD%A0%E5%BA%A6%E8%BF%87%E6%AF%8F%E4%B8%80%E4%B8%AA%E6%B7%B1%E5%A4%9C/id1524608462" options:nil completionHandler:nil];.
版本更新机制越早实现越好,防止有不受控制的版本存在。
通过苹果商店的自动更新机制进行自动更新是,应用更新时,手机会收到苹果商店的通知,并启动应用程序的自动更新下载应用并更新。但是可能出现手机收不到版本更新通知(由手机系统自己实现,并非弹出用户可见的push提示语)的情况和部分应用更新失败的情况,若你没有 Apple ID那么你也使用不了这类官方更新了。最麻烦的是用户一般都关闭了应用的自动更新和大批量自动更新时必然有部分失败,并且失败了就不会再自动更新成功,大都不希望在外面的移动蜂窝网络下自动更新大量应用,那样你流量会嗖嗖的莫名向上涨。
看来是自己的应用自己对自己负责更靠谱。版本更新一般分两类:强制更新和非强制性更新。当然强制更新是你不更新就不能用了,所以虽然霸道,但是也没有办法,通常是业务逻辑的大变更才会出现。非强制更新一般是不更新也能用,但是可能不更新部分功能你用不了或看不到,一般给你提供去苹果商店更新还是继续使用的选择。当然这种强制更新本质是有服务器来判断,需要拿当前版本号(服务器数据库中存储的应用版本号)和应用发送的版本号(存储在服务器与应用之间的相关会话中)相比较。当然服务器中的应用版本号在更新时要保证只能增大不能减少,若服务器的版本号变更需要向所有应用发送版本更新push通知。由于苹果审核应用的时间不能完全确定,并且苹果不希望有超过他掌控的版本更新,他做的又太完美,只能忽悠他了,等你收到你的应用上架的邮件时再更新你服务中数据库对应应用版本号了。
5.获取苹果商店对应app的网页,并解析出苹果商店该app的版本号,来显示强制版本更新。一般是非强制更新,无强制更新。
版本更新有五种实现:
第一种实现和场景,称为登录时版本更新(包括强制更新和非强制更新),用户启动app登录或自动登录时,发送请求需要带本应用的当前版本号,服务返回的结果中包含版本更新信息,并且一般立刻弹出提示模态对话框让用户选择操作,当然强制更新只有去苹果商店一条路。模态对话框在点击后跳到苹果商店,由于跳转的过程中,应用被自动切换到后台,所以若是强制更新,从后台切换到前台时,由于以前的模态对话框由于点击消失还需要再弹出一个。非强制更新就不需要弹了该干啥就干啥去。版本更新弹出框最好是一个高于状态栏的window窗口,这样它就能悬浮在任意页面之上,点击强制更新的更新按钮不销毁window窗口,点击非强制更新的更新按钮就销毁window窗口。
判断强制,只需要看Version的字符串的第1个".“前的字符串就可以,非强更新就看ersion的字符串的第1和第2”.“间的字符串,第3个”.“后的字符串都是不需要更新的子版本号。如:强制更新数字字符串.非强制更新数字字符串.普通子版本数字字符串.普通子版本数字字符串。7.3.2就是,7是强制更新版本号,3是非强制更新版本号,2是普通子版本号。原来的版本号比7小就是强制更新,原来的版本号比3小就是非强制更新,原来的版本号比7和3都一样其它后面的字符串比2小都不需要更新。所以想让你的APP具有强制更新和非强制更新,你的第一个版本就要支持这套机制,不然别人装了你的第一个最老的APP不更新,那么理论上都能用那个早期的逻辑,你后期的APP再支持这套更新机制也管不了最早期的版本。经过实际发布版本发现,苹果只允许发布含有2个’.‘或以下的版本号,不支持含有三个’.‘或以上的版本号。
针对启动时版本更新有一个例子,见文章《第三方授权的应用苹果审核被驳回解决方案和app版本更新》。
第二种实现和场景,称为PUSH信息版本更新,属于运行时版本更新(通常只包括强制更新)的一种,当用户在使用时,应用有新版本上架(准确的说是服务器数据库中对应应用版本号更新),你的服务推送在线用户push消息(强制更新才发送,非强制更新不发这种强制更新push消息),当用户收到这个强制更新push消息,不弹出PUSH消息,而是默默的把它记录到记录到全局变量中,当然不能弹出模态提示框,那样可能中断用户正在进行的操作,很不友好,当用户操作某些核心业务事件(抢单,发单等)时才弹出强制更新。另外可以在从后台切换到前台时弹出,注意要考虑到操作了一半,有电话过来,去接电话了,若弹出强制更新你以前做的事情都泡汤了,所以这种情况就看你的选择了。
第三种实现和场景,心跳版本检测,,属于运行时版本更新(通常只包括强制更新)的一种。若有的应用有心跳,当有版本更新是通过心跳的回调发送给客户端,客户端存在全局变量里,我们又不知道用户当前具体再做什么,所以最好别中断用户的当前操作,只有当用户点击重要业务逻辑(如:接单或发单)才弹出强制更新。
第四种实现和场景,在应用的设置里增加版本检测与更新。这种方案已经在2015年3月20日把它放倒了。我们发布的app就是因为这被驳回过。苹果审核时不能有这样的功能。只所以拎出来说说是因为有部分以前发布的应用现在仍旧有这类更新。
判断强制,只需要看Version的字符串的第1个”.“前的字符串就可以,非强更新就看ersion的字符串的第1和第2”.“间的字符串,第3个”."后的字符串都是不需要更新的子版本号。如:强制更新数字字符串.非强制更新数字字符串.普通子版本数字字符串.普通子版本数字字符串。7.3.2.4就是,7是强制更新版本号,3是非强制更新版本号,2是普通子版本号,4普通二级子版本号。原来的版本号比7小就是强制更新,原来的版本号比3小就是非强制更新,原来的版本号比7和3都一样其它后面的字符串比2或4小都不需要更新。所以想让你的APP具有强制更新和非强制更新,你的第一个版本就要支持这套机制,不然别人装了你的第一个最老的APP不更新,那么理论上都能用那个早期的逻辑,你后期的APP再支持这套更新机制也管不了最早期的版本。
手机自动更新关闭,开启就不用再说了吧:
在设置中,左边目录往下啦,看到一个itunes store和app store,选择点击;
点击之后看到自动下载的项目:一、应用程序;二、更新;就是后台联网有更新的情况下自动下载应用程序和更新软件。
我们只要把开关关闭,如果需要更新进入app store选择手动更新即可,这样就不会无缘无故耗费流量了。特别是iphone,有时无意识的开着3g或4g网络(有些区域没有高级基站,信号弱时会自动切换到2g网络),骚包的人从来没有关过蜂窝通信,我是小市民,一般关闭蜂窝通信,需要时再打开。注意苹果由于是个萌萌达,服务器不在大陆,经常批量自动更新有部分应用自动更新失败(我统计统计过,一次自动更新10个应用,通常至少2个应用是失败的), 并且若它失败了,以后永远不会再自动成功了,除非再有更新的版本或你把自动更新关闭再打开,它才给你重新把所有需要更新的应用自动更新一遍。
登录时版本更新的部分代码如下:
AppDelegate.mm文件 - (void)applicationDidBecomeActive:(UIApplication *)application { // [HomeViewController checkNetWork]; FLDDLogDebug(@"g_versionState :%d, g_bVersionUpdatePrompt : %d", [User currentUser].g_versionState, [User currentUser].g_bVersionUpdatePrompt); if((NotReachable == [SelfUser currentSelfUser].networkStatus) && (g_showNoNetNotice)) { FLDDLogDebug(@"applicationDidBecomeActive : g_showNoNetNotice"); [HomeViewController checkNetWork]; } if((VERSION_STATE_FORCE_UPDATE == [User currentUser].g_versionState) && (!([User currentUser].g_bVersionUpdatePrompt)) && ([User currentUser].g_newVersionURL != nil)) { UIAlertView *alert=[[UIAlertView alloc]initWithTitle:@"版本更新" message:@"检测到最新版本,请及时更新" delegate:self cancelButtonTitle:@"更新" otherButtonTitles:nil]; alert.tag = 100; @try { [alert show]; [User currentUser].g_bVersionUpdatePrompt = YES; } @catch (NSException *exception) { } } } - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateVersionNotification:) name:UPDATE_VERSION_NOTIFICATION object:nil]; . . . } - (void)updateVersionNotification:(NSNotification *)notification { [AppDelegate registerForRemoteNotification]; if((VERSION_STATE_FORCE_UPDATE == [User currentUser].g_versionState) && ([User currentUser].g_newVersionURL != nil)) { UIAlertView *alert=[[UIAlertView alloc]initWithTitle:@"版本更新" message:@"检测到最新版本,请及时更新" delegate:self cancelButtonTitle:@"更新" otherButtonTitles:nil,nil]; alert.tag = 100; @try { [alert show]; [User currentUser].g_bVersionUpdatePrompt = YES; } @catch (NSException *exception) { } } else if((VERSION_STATE_REMIND_UPDATE == [User currentUser].g_versionState) && ([User currentUser].g_newVersionURL != nil)) { UIAlertView *alert=[[UIAlertView alloc]initWithTitle:@"版本更新" message:@"检测到最新版本,请及时更新" delegate:self cancelButtonTitle:@"放弃" otherButtonTitles:@"更新",nil]; alert.tag = 101; @try { [alert show]; } @catch (NSException *exception) { } } } -(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { if([alertView tag] == 100) { switch (buttonIndex) { case 0: //YES应该做的事 { [User currentUser].g_bVersionUpdatePrompt = NO; [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[User currentUser].g_newVersionURL]]; break; } } } else if([alertView tag] == 101) { switch (buttonIndex) { case 0://NO应该做的事 { //[User currentUser].g_bVersionUpdatePrompt = NO; [[NSNotificationCenter defaultCenter] postNotificationName:LOGIN_NOTIFICATION object:nil]; break; } case 1: //YES应该做的事 { //[User currentUser].g_bVersionUpdatePrompt = NO; [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[User currentUser].g_newVersionURL]]; break; } } } } HomeViewController.m文件 - (void)autoLogin { . . . [[NSNotificationCenter defaultCenter] postNotificationName:UPDATE_VERSION_NOTIFICATION object:nil]; . . . } - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { if (alertView.tag == 1010) { _isAlertCherkNet = NO; if (_needSendNotice) { if (self.isHaveNet) { _needSendNotice = NO; _isAlertCherkNet = NO; [[NSNotificationCenter defaultCenter] postNotificationName:UPDATE_VERSION_NOTIFICATION object:nil]; } else { if(!_isAlertCherkNet && !_isAlertSrvFail) { self.alertView = [[UIAlertView alloc] initWithTitle:nil message:@"当前无网络,请打开网络" delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil]; self.alertView.tag = 1010; _isAlertCherkNet = YES; [self.alertView show]; self.alertView = nil; } } } } } LoginViewController.m文件 -(void)btn{ . . . [[NSNotificationCenter defaultCenter] postNotificationName:UPDATE_VERSION_NOTIFICATION object:nil]; . . . } User.m文件 + (void)loginWithParams:(NSDictionary *)params block:(void (^)(NSArray *, NSError *))block { NSMutableDictionary *muParams = [NSMutableDictionary dictionaryWithDictionary:params]; NSString *version = [AppManager getVersion]; NSString *model = [AppManager getDeviceModel]; NSString *iOSVersion = [AppManager getIOSVersion]; [muParams setObject:version forKey:@"version"]; [muParams setObject:model forKey:@"phoneType"]; [muParams setObject:iOSVersion forKey:@"phoneSystem"]; [muParams setObject:g_loginCmd forKey:@"cmdCode"]; [[API shareAPI] GET:@"loginWithFixedPasswordJsonPhone.htm" params:muParams success:^(AFHTTPRequestOperation *operation, id responseObject) { // [[NSNotificationCenter defaultCenter] postNotificationName:REMOVE_LOCALNOTIFICATION_NOTIFICATION object:nil];//登录成功后,取消之前所有不是本用户的催单提醒 NSString *cookie = [operation.response.allHeaderFields objectForKey:@"Set-Cookie"];//保存Cookie(仅登录成功后保存) [AppManager setUserDefaultsValue:cookie key:@"cookie"]; NSDictionary *responseBody = [responseObject objectForKey:@"body"]; . . . [User currentUser].grabMessage = [[responseBody objectForKey:@"grabCityNotEqual"] toString]; [User currentUser].g_newVersionURL = nil; NSString *hasNewVersion= [[responseBody objectForKey:@"hasNewVersion"] toString]; if([hasNewVersion isEqualToString:@"0"]) { [User currentUser].g_versionState = VERSION_STATE_NO_UPDATE; } else { NSString *needUpgrade= [[responseBody objectForKey:@"needUpgrade"] toString]; if([needUpgrade isEqualToString:@"1"]) { [User currentUser].g_versionState = VERSION_STATE_FORCE_UPDATE; } else { [User currentUser].g_versionState = VERSION_STATE_REMIND_UPDATE; } NSString *downloadUrl= [[responseBody objectForKey:@"downloadUrl"] toString]; [User currentUser].g_newVersionURL = downloadUrl; } NSArray *districts = [[responseBody objectForKey:@"countyList"] objectForKey:@"rows"]; NSMutableArray *muArray = [NSMutableArray array]; for (NSDictionary *attribute in districts) { District *district = [[District alloc] initWithAttributes:attribute]; [muArray addObject:district]; } [User currentUser].districts = [NSArray arrayWithArray:muArray]; [[User currentUser] saveUserInfo]; // [AppManager addUserRemindNotification]; // [AppManager addCurrentOrderRemind]; if (block) { block([NSArray arrayWithArray:muArray], nil); } } failure:^(AFHTTPRequestOperation *operation, NSError *error) { [User currentUser].state = @"-1"; if (block) { block(@[], error); } }]; } + (void)reLoginWithParams:(NSDictionary *)params block:(void (^)(NSArray *, NSError *))block { NSMutableDictionary *muParams = [NSMutableDictionary dictionaryWithDictionary:params]; NSString *version = [AppManager getVersion]; NSString *model = [AppManager getDeviceModel]; NSString *iOSVersion = [AppManager getIOSVersion]; [muParams setObject:version forKey:@"version"]; [muParams setObject:model forKey:@"phoneType"]; [muParams setObject:iOSVersion forKey:@"phoneSystem"]; [muParams setObject:g_reloginCmd forKey:@"cmdCode"]; [AppManager setUserDefaultsValue:@"" key:@"cookie"]; [[API shareAPI] GET:@"reLoginWithFixedPasswordJsonPhone.htm" params:muParams success:^(AFHTTPRequestOperation *operation, id responseObject) { // [[NSNotificationCenter defaultCenter] postNotificationName:REMOVE_LOCALNOTIFICATION_NOTIFICATION object:nil];//登录成功后,取消之前所有不是本用户的催单提醒 NSString *cookie = [operation.response.allHeaderFields objectForKey:@"Set-Cookie"];//保存Cookie(仅登录成功后保存) FLDDLogDebug(@"login cookie\n ----------\n%@\n------------\n", cookie); [AppManager setUserDefaultsValue:cookie key:@"cookie"]; NSDictionary *responseBody = [responseObject objectForKey:@"body"]; . . . [User currentUser].g_newVersionURL = nil; NSString *hasNewVersion= [[responseBody objectForKey:@"hasNewVersion"] toString]; if([hasNewVersion isEqualToString:@"0"]) { [User currentUser].g_versionState = VERSION_STATE_NO_UPDATE; } else { NSString *needUpgrade= [[responseBody objectForKey:@"needUpgrade"] toString]; if([needUpgrade isEqualToString:@"1"]) { [User currentUser].g_versionState = VERSION_STATE_FORCE_UPDATE; } else { [User currentUser].g_versionState = VERSION_STATE_REMIND_UPDATE; } NSString *downloadUrl= [[responseBody objectForKey:@"downloadUrl"] toString]; [User currentUser].g_newVersionURL = downloadUrl; } NSArray *districts = [[responseBody objectForKey:@"countyList"] objectForKey:@"rows"]; NSMutableArray *muArray = [NSMutableArray array]; for (NSDictionary *attribute in districts) { District *district = [[District alloc] initWithAttributes:attribute]; [muArray addObject:district]; } [User currentUser].districts = [NSArray arrayWithArray:muArray]; for (NSDictionary *attribute in districts) { District *district = [[District alloc] initWithAttributes:attribute]; [muArray addObject:district]; } [[User currentUser] saveUserInfo]; // [AppManager addUserRemindNotification]; // [AppManager addCurrentOrderRemind]; if (block) { block([NSArray arrayWithArray:muArray], nil); } } failure:^(AFHTTPRequestOperation *operation, NSError *error) { [User currentUser].state = @"-1"; if (block) { block(@[], error); } }]; }
第二种场景,运行版本更新部分代码:
AppDelegate.mm文件 - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo { // [application setApplicationIconBadgeNumber:[[[userInfo objectForKey:@"aps"] objectForKey:@"badge"] intValue]]; [application setApplicationIconBadgeNumber:0]; // FLDDLogDebug(@"Receive Notify: %@", [userInfo JSONString]); FLDDLogDebug(@"%@",userInfo); NSString *alert = [[userInfo objectForKey:@"aps"] objectForKey:@"alert"]; self.dictPush = nil; self.dictPush =[NSDictionary dictionaryWithDictionary:[userInfo objectForKey:@"page"] ]; NSString* sreType =[self.dictPush valueForKey:@"type"]; if (![[[self.dictPush valueForKey:@"userTel"] toString]isEqualToString:[AppManager valueForKey:@"telephone"]]) { return; } if((sreType) && [sreType isEqualToString:@"mddy_needUpgrade"]) { NSString *url = [self.dictPush objectForKey:@"downloadUrl"]; if(url != nil) { g_needUpgrade = 1; g_downloadUrl = url; } return; } . . . } HomeViewController.m文件 #pragma mark发单 -(void)buttonSendPressed{ [self hiddenDetail]; if (NotReachable == [SelfUser currentSelfUser].networkStatus) { [MBProgressHUD hudShowWithStatus:self :@"当前网络不可用,请检查您的网络设置"]; return; } if((g_needUpgrade == 1) && (g_downloadUrl != nil)) { [[NSNotificationCenter defaultCenter] postNotificationName:UPDATE_HEART_VERSION_NOTIFICATION object:nil]; return; } . . . } -(void)buttonSoonSendPressed{ [self hiddenDetail]; if (NotReachable == [SelfUser currentSelfUser].networkStatus) { [MBProgressHUD hudShowWithStatus:self :@"当前网络不可用,请检查您的网络设置"]; return; } if(g_loginStat == LOGIN_STATE_LOGIN_SUCESS) { if (self.remainOrder == 0) { [MBProgressHUD hudShowWithStatus:self :@"抱歉,您今日发单量已达上限,明天再来吧!"]; return; } if (self.remainQuickOrder == 0) { [MBProgressHUD hudShowWithStatus:self :@"抱歉,您今日极速发单量已达上限,明天再来吧!"]; return; } } else { [MBProgressHUD hudShowWithStatus:self :@"网络比蜗牛还慢,挤不进去呀"]; [[NSNotificationCenter defaultCenter]postNotificationName:ONCELOGININ object:self]; return; } if((g_needUpgrade == 1) && (g_downloadUrl != nil)) { [[NSNotificationCenter defaultCenter] postNotificationName:UPDATE_HEART_VERSION_NOTIFICATION object:nil]; return; } . . . }
通过第三方平台实现版本更新,友盟的版本更新。
在AppDelegate.mm或AppDelegate.m文件中的- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions函数中增加如下代码:
NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@“CFBundleShortVersionString”];
[MobClick setAppVersion:version];
[MobClick checkUpdate];
友盟的版本demo,见友盟文章:
http://www.umeng.com/apps/4100008dd65107258db11ef4/upload
友盟版本FAQ友盟文章:
http://dev.umeng.com/auto-update/ios-doc/integration
第四种方案,app和服务器联合实现多版本检测测与更新(自己实现版本更新)方案。
可以实现不同的版本升级到最新版本采用不同的策略,部分版本可以实现最新强制更新,部分版本实现强制更新。
app登陆时把版本号发送给服务,服务器查询数据,若根据对应版本号的_updated,_force_updated的值和最新版本号的值来给客户端返回强制更新,是否有更新,更新地址等字段。若最新版本的两个字端都是N,N,app对应的记录是Y,Y,那么肯定是强制更新了;最新版本的两个字端都是Y,Y,那么肯定不需要更新了;若是Y,N,那么对应呢版本就是非强制更新了。服务器根据客户端发送的版本对应记录的更新状态返回对应的结果,客户端根据具体返回结果进行强制更新和非强制更新的逻辑。
大型的软件如:windows操作系统采用的和第四种方案一样。
经过实际使用这种不同的版本给予不同的升级方案的版本升级方案是最强大的。能够处理一个版本正在使用,另一个版本正在审核,审核中的版本(版本更新的返回值中增加-review字段)禁止版本检测,使用不同的地图key等问题。高德地图的免费key,只能免费试用500次访问,超过500次可能间歇性的停一天通过该免费key的访问高德地图(这就是有些应用在使用地图时,有些天不能看到地图的问题的原因,看来免费的是有代价的。若想你的软件更正规还时付费吧)。如我们的应用需要用到的高德地图需要高频率的访问,所以申请了一个付费没有次数限制的高德地图key,由于我们的版本没有稳定业务,变动较大,所以先用企业账号发布了一个app,用的是这个地图付费key,而苹果商店又放了一个app。企业帐户发布者证书建立的Bundle Identifier和用苹果发布者证书建立的Bundle Identifier是不能一样的。高德的一个key职能对用一个app的Bundle Identifier。这就出现了多应用可能要用一个高德地图key的鸡生蛋,蛋生鸡的问题。
解决方案时:
企业证书发布的app用的是付费key,苹果商店的地图用免费key,当需要全部转到苹果商店时,把数据库中对应的版本的下载地址指向苹果商店的下载地址,并设置为强制更新,数据库中苹果商店的那个对应版本的审核标志设置0,来让苹果商店的app启动高德付费地图key。
这个方案的缺点是:由于安装的应用是同一个应用还是另一个新的应用看的是Bundle Identifier,不是应用名称,这样从企业者证书发布的app升级到苹果商店的app就会出现两个应用。
第五种实现和场景简述:获取苹果商店对应app的网页,并解析出苹果商店该app的版本号,来显示强制版本更新。一般是非强制更新,无强制更新。由于app打开时获取苹果商店对应app的网页较慢,可能第一打开时,显示不出来更新信息。一般第二次打开应用时可以看到版本更新。有点是,由于苹果审核时打开的苹果商店的对应app的版本号肯定比审核的版本号低不会显示版本更新,不存在其它方案由于后台或第三方版本信息忘记更新导致的让审核人员看到版本更新弹窗问题而被拒绝。除了版本更新弹窗UI需要设计,其它机制是在不同app中应用。缺点也很显然,只能显示非强制更新,无法实现强制更新或非强制更新的动态控制。