功能介绍
在开发快易博的时候,有一个功能叫做“分享心情”【见下图】。它的主要功能是:用户可以一次发表一个微博(在人人网称之为新鲜事)到所有用户选择的绑定平台(其实就是通常所说的微博同步)。
进入之后:
这个功能实现起来并不难,说白了就是依次调用各个开放平台的关于“发表”相关的API就好了。但牵扯到几个给用户提供更好的“用户体验”的需求,就不得不使用多线程了,需求如下:
(1) 在图二对应的功能界面上,用户在发表完成之后,应该可以立即关闭;
(2) 在各个平台的独立发表界面,在发表完成之后,发表界面自动关闭,显示打开发表界面之前的一个界面。
注:分享心情界面其实实现的是一个多功能的发表界面,可以同步推送,也可以选择你想推送的平台。为了给用户提供方便,在你点进各个平台之后(比如新浪微博),它里面有单独的发表到该平台的发表界面来提供更多的功能,比如“@、##”等,见下图。
原先的处理方式
对于上面的需求(1),我原先的做法是,在用户点击右上角的发表按钮之后。用MBProgressHUD强制给当前界面设置一个“遮罩”,以让返回按钮不可点击。这样做的目的是为了停留在当前界面来等待执行各个开放平台提供的诸如“发表成功”、“发表失败”之类的回调协议,并且在回调协议里,通过在状态栏使用提示信息来提示用户是否成功。
假如让返回按钮可点击,用户有可能在发表完成之后立马选择返回,那么当前的UIViewController可能已经被释放,回调的时候就会导致应用崩溃。当然如果用MBProgressHUD
hold主整个界面,用户除了选择等待就没有别的选择,而回调协议执行的时机其实取决于很多不确定因素(比如当前的网速,如果附带了图片,还要看图片的大小,以及各个开放平台对于API调用的响应速度等等),可能很快,也可能会等个好几十秒。而一旦你考验用户的耐心,那就是在给自己埋定时炸弹了。
对于需求(2),比需求(1)的关闭速度更快。我需要让用户在发表完成之后,发表界面立即关闭。所以当前UIViewController根本等不到回调协议的调用就已经释放,状态栏提示也无法实现。当然,妥协的办法还是用MBProgressHUD显示个“请稍后…”之类的强制让界面无法响应用户,等回调完成在选择关闭。
现在的处理方式
从上面的分析可以看得出,我希望回调协议与当前发表界面的UIViewController对象的生命周期没有关系。那么只能让这些“发表”操作都不在主线程(UI线程)上执行,这也就实现了发表操作与发表功能界面的解耦。
设计与代码实现
在iOS中实现多线程的方式有几种,这里使用NSOperation以及NSOperationQueue来实现。
UML简图:
大致代码如下:
PublishOperation:
#import <Foundation/Foundation.h>
#import "MTStatusBarOverlay.h"
@interface PublishOperation : NSOperation
<
MTStatusBarOverlayDelegate
>
@property (nonatomic,copy) NSString* txt;
@property (nonatomic,copy) NSString *publishImgPath;
@property (nonatomic,assign) BOOL completed;
@property (nonatomic,assign) BOOL canceledAfterError;
@property (nonatomic,retain) MTStatusBarOverlay *overlay;
- (void)clearPublishedImage;
- (id)initWithOperateParams:(NSString*)content;
@end |
#import "PublishOperation.h"
@implementation PublishOperation
@synthesize completed;
@synthesize canceledAfterError;
@synthesize txt=_txt;
@synthesize overlay=_overlay;
@synthesize publishImgPath=_publishImgPath;
- (void)dealloc{
[_txt release],_txt=nil;
[_overlay release],_overlay=nil;
[_publishImgPath release],_publishImgPath=nil;
[super dealloc];
}
- (id)init{
if (self=[super init]) {
completed=NO;
canceledAfterError=NO;
[self initMTStatusBarOverlay];
}
return self;
}
- (id)initWithOperateParams:(NSString*)content{
if (self=[self init]) {
self.txt=content;
}
return self;
}
/*
*实例化工具栏提示组件
*/
-(void)initMTStatusBarOverlay{
_overlay = [MTStatusBarOverlay sharedInstance];
_overlay.animation = MTStatusBarOverlayAnimationFallDown;
_overlay.detailViewMode = MTDetailViewModeHistory;
_overlay.delegate=self;
}
- (void)main{
//操作既没有完成也没有因为发生错误而取消,则hold住当前run loop
//防止返回主线程使得代理方法无法执行
while (!(self.completed||self.canceledAfterError)) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]];
DebugLog(@"running!");
}
}
#pragma mark - other methods -
- (void)clearPublishedImage{
//清除緩存圖片
if (fileExistsAtPath(self.publishImgPath)) {
removerAtItem(self.publishImgPath);
}
}
@end |
SinaWeiboPublishOperation
#import "PublishOperation.h"
#import "WBEngine.h"
@interface SinaWeiboPublishOperation : PublishOperation
<
WBEngineDelegate
>
@end |
#import "SinaWeiboPublishOperation.h"
@interface SinaWeiboPublishOperation()
@property (nonatomic,retain) WBEngine *engine;
@end
@implementation SinaWeiboPublishOperation
@synthesize engine=_engine;
- (void)dealloc{
[_engine setDelegate:nil];
[_engine release],_engine=nil;
[super dealloc];
}
/*
*business logic
*/
- (void)main{
self.publishImgPath=PUBLISH_IMAGEPATH_SINAWEIBO;
_engine = [[WBEngine alloc]initWithAppKey:kWBSDKDemoAppKey
appSecret:kWBSDKDemoAppSecret];
_engine.delegate=self;
[self.engine sendWeiBoWithText:self.txt
image:[UIImage imageWithContentsOfFile:
PUBLISH_IMAGEPATH_SINAWEIBO]];
[self.overlay postImmediateMessage:@"新浪微博发表中..." animated:YES];
[super main];
}
#pragma mark - WBEngineDelegate Methods -
- (void)engine:(WBEngine *)engine requestDidSucceedWithResult:(id)result{
[self clearPublishedImage];
[GlobalInstance showOverlayMsg:@"新浪微博发表成功!"
andDuration:2.0
andOverlay:self.overlay];
self.completed=YES; |
[NSThread sleepForTimeInterval:5];
}
- (void)engine:(WBEngine *)engine requestDidFailWithError:(NSError *)error{
[self clearPublishedImage];
[GlobalInstance showOverlayErrorMsg:@"新浪微博发表失败!"
andDuration:2.0
andOverlay:self.overlay];
self.canceledAfterError=YES; |
[NSThread
sleepForTimeInterval:5]; |
RenRenPublishOperation
#import "PublishOperation.h"
@interface RenRenPublishOperation : PublishOperation
<
RenrenDelegate
>
@end |
#import "RenRenPublishOperation.h"
@implementation RenRenPublishOperation
/*
*business logic
*/
- (void)main{
self.publishImgPath=PUBLISH_IMAGEPATH_RENREN;
BOOL hasImage=[[NSFileManager defaultManager]fileExistsAtPath:PUBLISH_IMAGEPATH_RENREN];
if (hasImage) {
ROPublishPhotoRequestParam *photoParams=[[[ROPublishPhotoRequestParam alloc]init]autorelease];
photoParams.imageFile=[UIImage imageWithContentsOfFile:PUBLISH_IMAGEPATH_RENREN];
photoParams.caption=self.txt;
[[Renren sharedRenren] publishPhoto:photoParams andDelegate:self];
[self.overlay postImmediateMessage:@"人人图片上传中..." animated:YES];
}else {
NSMutableDictionary *params=[NSMutableDictionary dictionaryWithCapacity:10];
[params setObject:@"status.set" forKey:@"method"];
[params setObject:self.txt
forKey:@"status"];
[[Renren sharedRenren]requestWithParams:params andDelegate:self];
[self.overlay postImmediateMessage:@"人人状态发表中..." animated:YES];
}
[super main];
}
#pragma mark - RenrenDelegate (ren ren) -
-(void)renren:(Renren *)renren requestDidReturnResponse:(ROResponse *)response{
[self clearPublishedImage];
[GlobalInstance showOverlayMsg:@"人人新鲜事发表成功!"
andDuration:2.0
andOverlay:self.overlay];
self.completed=YES; |
[NSThread
sleepForTimeInterval:5]; |
TencentWeiboPublishOperation
#import "PublishOperation.h"
#import "TencentWeiboDelegate.h"
@interface TencentWeiboPublishOperation : PublishOperation
<
TencentWeiboDelegate
>
@end |
#import "TencentWeiboPublishOperation.h"
#import "TencentWeiboManager.h"
#import "OpenApi.h"
@implementation TencentWeiboPublishOperation
/*
*business logic
*/
- (void)main{
[self.overlay postImmediateMessage:@"腾讯微博发表中..." animated:YES];
self.publishImgPath=PUBLISH_IMAGEPATH_TENCENTWEIBO;
BOOL hasImage=[[NSFileManager defaultManager]fileExistsAtPath:PUBLISH_IMAGEPATH_TENCENTWEIBO];
OpenApi *myApi=[TencentWeiboManager getOpenApi];
myApi.delegate=self;
if (!hasImage) {
[myApi publishWeibo:self.txt
jing:@""
wei:@""
format:@"json"
clientip:@"127.0.0.1"
syncflag:@"0"];
}else{
[myApi publishWeiboWithImageAndContent:self.txt
jing:@"" wei:@""
format:@"json"
clientip:@"127.0.0.1"
syncflag:@"0"];
}
}
#pragma mark - tencent weibo delegate -
- (void)tencentWeiboPublishedSuccessfully{
[self clearPublishedImage];
[GlobalInstance showOverlayMsg:@"腾讯微博发表成功!"
andDuration:2.0
andOverlay:self.overlay];
[NSThread sleepForTimeInterval:5];
}
- (void)tencentWeiboRequestFailWithError{
[self clearPublishedImage];
[GlobalInstance showOverlayErrorMsg:@"腾讯微博发表失败!"
andDuration:2.0
andOverlay:self.overlay];
[NSThread sleepForTimeInterval:5];
}
@end |
注解:NSOperation在非并发的执行方式下,只需要重新实现它的main方法即可。
在PublishOperation的main方法中的那段代码的主要目的hold主当前的RunLoop(通俗一点讲其实就是阻塞当前执行的上下文,直到回调协议被执行),由此可见这里仍然存在回调协议无法执行的问题。为什么会出现这种情况,为什么要hold执行的上下文?因为各个平台的API通常都是异步发送请求的(腾讯微博除外),等到有响应了再来执行实现了的协议。而所谓的异步,其实也是操作系统在后台用另一个线程来处理的。而NSOperation的执行模式是它执行完实现在main方法里的逻辑就会退出,返回主线程。所以通常情况下这里的回调协议还是没有得到机会执行(机会其实是得到了,只是当前的NSOperation对象已被释放,导致内存访问错误)。所以这里用一个while循环hold主当前的runloop,一直保持到各个平台的异步回调完协议之后,设置completed或者canceledAfterError为YES,以退出while循环。注意这两个属性是异步设置的,也就是说是另一个线程来设置的。这里说明一下,在各个平台调用了[super
main];方法之后,其实你不在回调协议中(或者说在另一个线程中)是无法设置这两个属性的。因为[super
main];这句让当前执行NSOperation的线程阻塞了。
我顺便吐槽一下腾讯微博开放平台,它们开放的Framework中,发出的请求居然是同步获取结果的,而且它们居然都没有定义任何的回调协议,这里也让我很费神。所以在TencentWeiboOperation中的实现会跟新浪、人人有所不同。因为调用是同步的,所以在TencentWeiboPublishOperation最后直接调用[super
main];是没用的(因为它会在回调协议之后才会执行,所以这边不需要调用它) ,这边调用线程的Sleep方法,让线程睡眠5秒钟,也就是延迟当前的NSOperation上下文5秒,延迟是因为在状态栏显示提示信息需要2秒,如果不延迟,那么在completed设置为YES之后NSOperation就会执行完成并退出,那MTStatusOverlayDelegate也没有机会执行。
/*
*状态栏显示完成提示信息
*/
-(void)showOverlayMsg:(NSString*)msg andDuration:(NSTimeInterval)duration andOverlay:(MTStatusBarOverlay*)overlay{
[overlay postImmediateFinishMessage:msg
duration:duration animated:YES];
} |
你在应用程序的AppDelegate中定义一个NSOperationQueue,用来执行NSOperation,就可以完成对发表操作与发表功能界面的Controller解耦。这样就可以在发表完成之后,直接关闭并释放Controller,并且由于功能的封装,它变成一个独立的、可复用的整体。在“分享心情”功能界面里,多平台同步推送的话,你只需要把各个平台的“PublishOperation”丢到NSOperationQueue去,其他就不用管,直接关闭或者返回即可。
新浪微博单独发表界面示例代码:
- (void)publishBtn_handle:(id)sender{
NSString *txt=self.publishTxtView.text;
PublishOperation *publishOperation=[[[SinaWeiboPublishOperation alloc]initWithOperateParams:txt]autorelease];
[((FastEasyBlogAppDelegate*)appDelegateObj).operationQueue addOperation:publishOperation];
[self dismissModalViewControllerAnimated:YES];
} |
分享心情发表界面示例代码:
/*
*发送
*/
-(void)publish_click{
//移除高亮效果
UIButton *btn=(UIButton*)self.navigationItem.rightBarButtonItem.customView;
[btn setBackgroundImage:[UIImage imageNamed:@"publishBtn.png"] forState:UIControlStateNormal];
if ([self.publishTxtView.text length]==0) {
[GlobalInstance showMessageBoxWithMessage:@"请说点什么吧!"];
return;
}
NSString *weiboTextContent=self.publishTxtView.text;
if (plaformSwitch.isRenRenOpen) {
PublishOperation *renrenOperation=[[[RenRenPublishOperation alloc]initWithOperateParams:
weiboTextContent]autorelease];
[((FastEasyBlogAppDelegate*)appDelegateObj).operationQueue addOperation:renrenOperation];
}
if (plaformSwitch.isTencentWeiboOpen) {
PublishOperation *tencentOperation=[[[TencentWeiboPublishOperation alloc]initWithOperateParams:
weiboTextContent]autorelease];
[((FastEasyBlogAppDelegate*)appDelegateObj).operationQueue addOperation:tencentOperation];
}
//清空內容
[self clearInputFromTextView];
UIButton *imgBtn=(UIButton*)[self.view viewWithTag:4321];
[imgBtn removeFromSuperview];
[self dismissModalViewControllerAnimated:YES];
} |
|