移动端技术的研发思考及最佳实践
署名2021-02-03

移动端技术的研发思考及最佳实践,超级App的性能和代码痛点:在iOS开发中线程使用特别方便,但是多线程使用不当引发的崩溃问题很多,多线程访问引发野指针问题,多线程访问引发容器类崩溃问题,多线程访问引发过渡释放问题,以手机淘宝为例,整个生命周期大量使用线程,多线程使用不当引发的崩溃问题占比达到了60%以上。  

24.png

1、为了解决多线程崩溃问题或者为了让代码可读性更强可能会严重牺牲应用性能。iOS系统API设计很不友好,绝大部分IO、跨进程调用等耗时接口提供的都是同步方法,主线程调用会产生严重性能问题。为了解决多线程崩溃加的锁、信号量等,由于设计不合理,很容易引发卡顿甚至死锁。iOS系统API缺乏统一的异步编程模型,Delegate、Callback、同步等杂揉在一起,要写出高性能代码需要付出极大的努力。  

2、系统API、IO等接口在异步编程上支持并不友好,极易产生性能问题。 

iOS异步编程现状。  

3、基于Block回调的异步编程方式是目前iOS开发使用最广泛的异步编程方式,下面是使用block回调的异步编程的一个例子:  

[NSURLConnectionsendAsynchronousRequest:rqqueue:nilcompletionHandler:^(NSURLResponse*_Nullableresponse,NSData*_Nullabledata,NSError*_NullableconnectionError){  

if(connectionError){  

if(callback){  

callback(nil,nil,connectionError);  

}  

}  

else{  

dispatch_async(dispatch_get_global_queue(0,0),^{  

NSDictionary*dict=[NSJSONSerializationJSONObjectWithData:dataoptions:0error:nil];  

NSString*imageUrl=dict[@"image"];  

[NSURLConnectionsendAsynchronousRequest:[NSURLRequestrequestWithURL:[NSURLURLWithString:imageUrl]]queue:nilcompletionHandler:^(NSURLResponse*_Nullableresponse,NSData*_Nullabledata,NSError*_NullableconnectionError){  

dispatch_async(dispatch_get_global_queue(0,0),^{  

if(connectionError){  

callback(nil,dict,connectionError);  

}  

else{  

UIImage*image=[[UIImagealloc]initWithData:data];  

if(callback){  

(image,dict,nil);  

}  

}  

});  

}];  

});  

}  

}];  

基于Block回调的异步编程方式有以下缺点:容易进入"嵌套地狱"、错误处理复杂和冗长、容易忘记调用completionhandler、条件执行变得很困难、从互相独立的调用中组合返回结果变得极其困难、在错误的线程中继续执行(如子线程操作UI)、其他语言的异步编程方式。  

4、kotlin中通过协程实现的异步编程方式,代码简洁,逻辑清晰。  

5、node.js中通过协程async/await方式实现的可重试异步网络请求。协程不仅打开了异步编程的大门,还提供了大量其他的可能性。协程是什么?  

6、协程具有以下特征。  

7、协程的概念在60年代就已经提出,目前在服务端中应用比较广泛,在高并发场景下使用极其合适,可以极大降低单机的线程数,提升单机的连接和处理能力,但是在iOS移动开发中并没有框架支持。从其他语言的发展来看,基于协程的全新的异步编程方式,是我们解决现有异步编程问题的有效的方式,但是无奈苹果对于Objective-C的支持基本已经停滞,也不指望苹果能够让Objective-C的开发者用上协程,基于我们团队长期对系统底层库和汇编的研究,我们通过汇编和C语言实现了支持Objective-C和Swift协程的完美解决方案coobjc  

iOS协程开发框架——coobjc,coobjc是由手机淘宝架构团队推出的能在iOS上使用的协程开发框架,目前支持Objective-C和Swift中使用,我们底层使用汇编和C语言进行开发,上层进行提供了Objective-C和Swift的接口,目前以Apache开源协议进行了开源,开源项目地址https://github.com/alibaba/coobjc  

8、coobjc框架设计:最底层是协程内核,包含了栈切换的管理、协程调度器的实现、协程间通信channel的实现等。中间层是基于协程的操作符的包装,目前支持async/await、Generator、Actor等编程模型。最上层是对系统库的协程化扩展,目前基本上覆盖了Foundation和UIKit的所有IO和耗时方法。async/await操作符。  

9、(引用自:https://dkandalov.github.io/async-await):await的作用,在协程函数中通过await调用异步方法,当前线程的执行流会立即返回,不会阻塞当前线程的执行,当异步方法执行结束后,await会恢复当前协程函数的执行,这样在协程内部是顺序执行,但是协程不会阻塞当前线程其他代码的执行  

创建协程。  

我们使用co_launch方法创建协程  

co_launch(^{  

...  

});  

co_launch创建的协程默认在当前线程进行调度  

await异步方法  

在协程中我们使用await方法等待异步方法执行结束,得到异步执行结果  

-(void)viewDidLoad{  

...  

co_launch(^{  

NSData*data=await(downloadDataFromUrl(url));  

UIImage*image=await(imageFromData(data));  

self.imageView.image=image;  

});  

}  

上述代码将原本需要dispatch_async两次的代码变成了顺序执行,代码更加简洁  

使用场景  

协程最重要的使用场景就是异步计算(在C#等语言中通过async/await进行处理)。我们先看一个通过传统的callback进行异步I/O的场景:  

//从网络异步加载数据  

[NSURLSessionsharedSession].configuration.requestCachePolicy=NSURLRequestReloadIgnoringCacheData;  

NSURLSessionDownloadTask*task=[[NSURLSessionsharedSession]downloadTaskWithURL:urlcompletionHandler:  

^(NSURL*location,NSURLResponse*response,NSError*error){  

if(error){  

return;  

}  

//在子线程解析数据,并生成图片  

dispatch_async(dispatch_get_global_queue(0,0),^{  

NSData*data=[[NSDataalloc]initWithContentsOfURL:location];  

UIImage*image=[[UIImagealloc]initWithData:data];  

dispatch_async(dispatch_get_main_queue(),^{  

//调度到主线程显示图片  

imageView.image=image;  

});  

});  

}];  

上面是iOS开发中常见的异步调用方式,我们经常需要在callback中嵌套callback,代码的缩进和逻辑变得越来越复杂,代码可读性和可维护性会随着回调的嵌套层级增长变得越来越差,进入所谓的"callbackhell"(嵌套地狱)  

同样的异步计算,使用协程可以很直接的表达出来(需要有库提供了满足协程需要的I/O接口):  

co_launch(^{  

NSData*data=await(downloadDataFromUrl(url));  

UIImage*image=await(imageFromData(data));  

imageView.image=image;  

});  

Generator  

10、协程的另一个经典的使用场景就是懒计算序列(在C#、Python等语言中通过yield来处理)。这个懒计算序列可以通过顺序执行的代码生成,只有在需要的时候才进行计算:  

COSequence*fibonacci=sequence(^{  

yield(@(1));  

intcur=1;  

intnext=1;  

while(1){  

yield(@(next));  

inttmp=cur+next;  

cur=next;  

next=tmp;  

}  

});  

这个代码创建了懒加载的斐波拉契序列,我们可以获取序列的值,通过take或者next:  

for(idvalinfibonacci){  

NSLog(@"%@",val);  

}  

idval=[fibonaccinext];  

NSArray*list=[fibonaccitake:5]  

传统容器类与Generator的区别  

11、创建Generator:

我们使用co_sequence创建Generator  

COCoroutine*co1=co_sequence(^{  

intindex=0;  

while(co_isActive()){  

yield_val(@(index));  

index++;  

}  

});  

在其他协程中,我们可以调用next方法,获取生成器中的数据  

co_launch(^{  

for(inti=0;i<10;i++){  

val=[[co1next]intValue];  

}  

});  

使用场景  

生成器可以在很多场景中进行使用,比如消息队列、批量下载文件、批量加载缓存等:  

intunreadMessageCount=10;  

NSString*userId=@"xxx";  

COSequence*messageSequence=sequenceOnBackgroundQueue(@"message_queue",^{  

//在后台线程执行  

while(1){  

yield(queryOneNewMessageForUserWithId(userId));  

}  

});  

//主线程更新UI  

co(^{  

for(inti=0;i<unreadMessageCount;i++){  

if(!isQuitCurrentView()){  

displayMessage([messageSequencetake]);  

}  

}  

});  

通过生成器,我们可以把传统的生产者加载数据-》通知消费者模式,变成消费者需要数据-》告诉生产者加载模式,避免了在多线程计算中,需要使用很多共享变量进行状态同步,消除了在某些场景下对于锁和信号量的使用。  

12、我们需要设置Delegate,在Delegate中处理所有的解析逻辑:

13、我们可以在一个循环里遍历解析了,更加简单方便,尤其对于大的XML文件,我们可以只解析一部分,然后就cancel,这样可以更加节省内存。Actor的概念来自于Erlang,在AKKA中,可以认为一个Actor就是一个容器,用以存储状态、行为、Mailbox以及子Actor与Supervisor策略。Actor之间并不直接通信,而是通过Mail来互通有无。  

14、mailbox:存储message的队列:  

IsolatedState:actor的状态,内部变量等。message:类似于OOP的方法调用的参数。  

Actor模型的执行方式有两个特点:每个Actor,单线程地依次执行发送给它的消息。不同的Actor可以同时执行它们的消息。  

创建Actor,我们可以使用co_actor_onqueue在指定线程创建actor  

CCOActor*actor=co_actor_onqueue(^(CCOActorChan*channel){  

...//定义actor的状态变量  

for(CCOActorMessage*messageinchannel){  

...//处理消息  

}  

},q);  

给actor发送消息  

actor的send方法可以给actor发送消息  

CCOActor*actor=co_actor_onqueue(^(CCOActorChan*channel){  

...//定义actor的状态变量  

for(CCOActorMessage*messageinchannel){  

...//处理消息  

}  

},q);  

//给actor发送消息  

[actorsend:@"sadf"];  

[actorsend:@(1)];  

使用场景:现有的面向对象编程设计的思路,很容易在多线程引发的崩溃问题,因为类的方法和属性都是暴露给调用方,调用方可以在任何线程进行调用,但是该线程往往并不是库的提供者设想的线程,就很容易出现崩溃。从理论上来说,我们可以通过合理的设计来让多线程任务执行变得趋于合理,同时通过丰富的文档和示例告诉使用方该如何正确的调用我们设计的接口,但是这种通过依赖人工设计和文档来解决问题并不彻底,终究会因为疏忽而引发新的问题。  

15、使用Actor编程模型可以帮助我们很好的设计出线程安全的模块,使用传统的方式,如果我们要实现一个计数器。  

16、传统的方式通过锁来确保线程安全,那使用Actor,我们可以使用如下方式实现:  

COActor*countActor=co_actor_onqueue(get_test_queue(),^(COActorChan*channel){  

intcount=0;  

for(COActorMessage*messageinchannel){  

if([[messagestringType]isEqualToString:@"inc"]){  

count++;  

}  

elseif([[messagestringType]isEqualToString:@"get"]){  

message.complete(@(count));  

}  

}  

});  

对于上述actor实现的计数器,可以按照如下方式使用:  

co_launch(^{  

[countActorsendMessage:@"inc"];  

[countActorsendMessage:@"inc"];  

[countActorsendMessage:@"inc"];  

intcurrentCount=[await([countActorsendMessage:@"get"])intValue];  

NSLog(@"count:%d",currentCount);  

});  

co_launch_onqueue(dispatch_queue_create("counterqueue1",NULL),^{  

[countActorsendMessage:@"inc"];  

[countActorsendMessage:@"inc"];  

[countActorsendMessage:@"inc"];  

[countActorsendMessage:@"inc"];  

intcurrentCount=[await([countActorsendMessage:@"get"])intValue];  

NSLog(@"count:%d",currentCount);  

});  

在两个不同线程中进行调用,完全不用担心线程安全  

Swift的支持  

通过上层的封装,coobjc完全支持Swift,让我们可以在Swift中提前享用协程。  

由于Swift更丰富、更高级的语法支持,coobjc在Swift中使用更优雅,例如:  

functest(){  

co_launch{//创建协程  

//异步获取数据  

letresultStr=tryawait(channel:co_fetchSomething())  

print("result:\(resultStr)")  

}  

co_launch{//创建协程  

//异步获取数据  

letresult=tryawait(promise:co_fetchSomethingAsynchronous())  

switchresult{  

case.fulfilled(letdata):  

print("data:\(String(describing:data))")  

break  

case.rejected(leterror):  

print("error:\(error)")  

}  

}  

}  

使用coobjc的实际案例  

我们以GCDFetchFeed开源项目中Feeds流更新的代码为例,演示一下协程的实际使用场景和优势,下面是原始的不使用协程的实现:  

-(RACSignal*)fetchAllFeedWithModelArray:(NSMutableArray*)modelArray{  

@weakify(self);  

return[RACSignalcreateSignal:^RACDisposable*(id<RACSubscriber>subscriber){  

@strongify(self);  

//创建并行队列  

dispatch_queue_tfetchFeedQueue=dispatch_queue_create("com.starming.fetchfeed.fetchfeed",DISPATCH_QUEUE_CONCURRENT);  

dispatch_group_tgroup=dispatch_group_create();  

self.feeds=modelArray;  

for(inti=0;i<modelArray.count;i++){  

dispatch_group_enter(group);  

SMFeedModel*feedModel=modelArray[i];  

feedModel.isSync=NO;  

[selfGET:feedModel.feedUrlparameters:nilprogress:nilsuccess:^(NSURLSessionTask*task,idresponseObject){  

dispatch_async(fetchFeedQueue,^{  

@strongify(self);  

//解析feed  

self.feeds[i]=[self.feedStoreupdateFeedModelWithData:responseObjectpreModel:feedModel];  

//入库存储  

SMDB*db=[SMDBshareInstance];  

@weakify(db);  

[[dbinsertWithFeedModel:self.feeds[i]]subscribeNext:^(NSNumber*x){  

@strongify(db);  

SMFeedModel*model=(SMFeedModel*)self.feeds[i];  

model.fid=[xintegerValue];  

if(model.imageUrl.length>0){  

NSString*fidStr=[xstringValue];  

db.feedIcons[fidStr]=model.imageUrl;  

}  

//插入本地数据库成功后开始sendNext  

[subscribersendNext:@(i)];  

//通知单个完成  

dispatch_group_leave(group);  

}];  

});//enddispatchasync  

}failure:^(NSURLSessionTask*operation,NSError*error){  

NSLog(@"Error:%@",error);  

dispatch_async(fetchFeedQueue,^{  

@strongify(self);  

[[[SMDBshareInstance]insertWithFeedModel:self.feeds[i]]subscribeNext:^(NSNumber*x){  

SMFeedModel*model=(SMFeedModel*)self.feeds[i];  

model.fid=[xintegerValue];  

dispatch_group_leave(group);  

}];  

});//enddispatchasync  

}];  

}//endfor  

//全完成后执行事件  

dispatch_group_notify(group,dispatch_get_main_queue(),^{  

[subscribersendCompleted];  

});  

returnnil;  

}];  

}  

下面是viewDidLoad中对上述方法的调用:  

[UIApplicationsharedApplication].networkActivityIndicatorVisible=YES;  

self.fetchingCount=0;//统计抓取数量  

@weakify(self);  

[[[[[[SMNetManagershareInstance]fetchAllFeedWithModelArray:self.feeds]map:^id(NSNumber*value){  

@strongify(self);  

NSUIntegerindex=[valueintegerValue];  

self.feeds[index]=[SMNetManagershareInstance].feeds[index];  

returnself.feeds[index];  

}]doCompleted:^{  

//抓完所有的feeds  

@strongify(self);  

NSLog(@"fetchcomplete");  

//完成置为默认状态  

self.tbHeaderLabel.text=@"";  

self.tableView.tableHeaderView=[[UIViewalloc]init];  

self.fetchingCount=0;  

//下拉刷新关闭  

[self.tableView.mj_headerendRefreshing];  

//更新列表  

[self.tableViewreloadData];  

//检查是否需要增加源  

if([SMFeedStoredefaultFeeds].count>self.feeds.count){  

self.feeds=[SMFeedStoredefaultFeeds];  

[selffetchAllFeeds];  

}  

//缓存未缓存的页面  

[selfcacheFeedItems];  

}]deliverOn:[RACSchedulermainThreadScheduler]]subscribeNext:^(SMFeedModel*feedModel){  

//抓完一个  

@strongify(self);  

self.tableView.tableHeaderView=self.tbHeaderView;  

//显示抓取状态  

self.fetchingCount+=1;  

self.tbHeaderLabel.text=[NSStringstringWithFormat:@"正在获取%@...(%lu/%lu)",feedModel.title,(unsignedlong)self.fetchingCount,(unsignedlong)self.feeds.count];  

feedModel.isSync=YES;  

[self.tableViewreloadData];  

}];  

上述代码无论从可读性还是简洁性上都比较差,下面我们看一下使用协程改造以后的代码:  

-(SMFeedModel*)co_fetchFeedModelWithUrl:(SMFeedModel*)feedModel{  

feedModel.isSync=NO;  

idresponse=await([selfco_GET:feedModel.feedUrlparameters:nil]);  

if(response){  

SMFeedModel*resultModel=await([selfco_updateFeedModelWithData:responsepreModel:feedModel]);  

intfid=[[SMDBshareInstance]co_insertWithFeedModel:resultModel];  

resultModel.fid=fid;  

if(resultModel.imageUrl.length>0){  

NSString*fidStr=[@(fid)stringValue];  

[SMDBshareInstance].feedIcons[fidStr]=resultModel.imageUrl;  

}  

returnresultModel;  

}  

intfid=[[SMDBshareInstance]co_insertWithFeedModel:feedModel];  

feedModel.fid=fid;  

returnnil;  

}  

下面是viewDidLoad中使用协程调用该接口的地方:  

co_launch(^{  

for(NSUIntegerindex=0;index<self.feeds.count;index++){  

SMFeedModel*model=self.feeds[index];  

self.tableView.tableHeaderView=self.tbHeaderView;  

//显示抓取状态  

self.tbHeaderLabel.text=[NSStringstringWithFormat:@"正在获取%@...(%lu/%lu)",model.title,(unsignedlong)(index+1),(unsignedlong)self.feeds.count];  

model.isSync=YES;  

//协程化加载数据  

SMFeedModel*resultMode=[[SMNetManagershareInstance]co_fetchFeedModelWithUrl:model];  

if(resultMode){  

self.feeds[index]=resultMode;  

[self.tableViewreloadData];  

}  

}  

self.tbHeaderLabel.text=@"";  

self.tableView.tableHeaderView=[[UIViewalloc]init];  

self.fetchingCount=0;  

//下拉刷新关闭  

[self.tableView.mj_headerendRefreshing];  

//更新列表  

[self.tableViewreloadData];  

//检查是否需要增加源  

[selfcacheFeedItems];  

});  

协程化改造之后的代码,变得更加简单易懂,不易出错。  

协程的优势:  

(1)简明  

概念少:只有很少的几个操作符,相比响应式几十个操作符,简直不能再简单了  

原理简单:协程的实现原理很简单,整个协程库只有几千行代码  

(2)易用  

使用简单:它的使用方式比GCD还要简单,接口很少  

改造方便:现有代码只需要进行很少的改动就可以协程化,同时我们针对系统库提供了大量协程化接口  

(3)清晰  

同步写异步逻辑:同步顺序方式写代码是人类最容易接受的方式,这可以极大的减少出错的(4)概率  

可读性高:使用协程方式编写的代码比block嵌套写出来的代码可读性要高很多  

(5)性能  

调度性能更快:协程本身不需要进行内核级线程的切换,调度性能快,即使创建上万个协程也毫无压力  

减少卡顿卡死:协程的使用以帮助开发减少锁、信号量的滥用,通过封装会引起阻塞的IO等协程接口,可以从根源上减少卡顿、卡死,提升应用整体的性能  

程序是写来给人读的,只会偶尔让机器执行一下——AbelsonandSussman。基于协程实现的编程范式能够帮助开发者编写出更加优美、健壮、可读性更强的代码。协程可以帮助我们在编写并发代码的过程中减少线程和锁的使用,提升应用的性能和稳定性。