iOS:应用程序的线程安全性

简介: 本文在于说明iOS应用的Objective-C代码的线程安全性。先是简单介绍一下线程安全的基本知识,然后通过一个小例子来观察非线程安全代码,最后会稍稍介绍一个可以用来分析线程安全隐患的工具。
本文在于说明iOS应用的Objective-C代码的线程安全性。先是简单介绍一下线程安全的基本知识,然后通过一个小例子来观察非线程安全代码,最后会稍稍介绍一个可以用来分析线程安全隐患的工具。

1) 基础知识 (Threading Basics)


当启动一个应用时,iOS会对应创建一个进程(process)和一块为之分配的内存。简单地说,一个应用进程的内存包括三个部分:  (更详细的描述可以看 这里 ):

程序内存( program memory)存储应用的执行代码,它在执行时由一个指令指针(Instruction Pointer, IP)来跟踪程序执行位置。

堆( heap )存储由 [… alloc] init]来创建的对象。

堆栈( stack )则用于函数调用。存储参数和函数的局部变量。

一个应用进程默认有一个主线程。如果有多线程,所有线程共享 program memory  和  heap  , 每个线程又有各自的IP和堆栈。就是说每个线程都有自己的执行流程,当它呼叫一个方法时,其它线程是无法访问调用参数和该方法的局部变量的。而那些在堆(heap)上创建的对象却可以被其它线程访问和使用。

2) 实验 (Experiment)


建个使用如下代码的小程序:
 @interface FooClass {}  
 @property (nonatomic, assign) NSUInteger value;  
 - (void)doIt;  
 @end  
   
 @implementation FooClass  
 @synthesize value;  
   
 - (void)doIt {  
      self.value = 0;  
      for (int i = 0; i < 100000; ++i) {  
           self.value = i;  
      }  
      NSLog(@"执行后: %d (%@)", self.value, [NSThread currentThread]);  
 }  
 @end

这个类有一个整型属性value,并且会在doIt方法被连续增加100000次。执行完后,再将它的值和调用doIt方法的线程信息输出出来。 如下在AppDelegate中增加一个 _startExperiment方法,然后在 application:didFinishLaunchingWithOptions:方法中调用它 :
 - (void)_startExperiment {  
      FooClass *foo = [[FooClass alloc] init];  
      [foo doIt];  
      [foo release];  
 }  
   
 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {  
      // …  
      [self _startExperiment];  
      return YES;       
 }

因为这里还有多线程,所以结果很简单地显示value值为99999。

3) 线程安全 (Thread Safety)


如下以多线程并行执行doIt():
- (void)_startExperiment {  
      FooClass *foo = [[FooClass alloc] init];  
      dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);  
     
   for (int i = 0; i < 4; ++i) {  //四个线程
     dispatch_async(queue, ^{  
       [foo doIt];  
     });  
   }  
   [foo release];  
 }

 
再执行,你的输出可能会类似如下的结果: (实际可能不一样):
 after execution: 19851 (NSThread: 0x6b29bd0>{name = (null), num = 3})  
 after execution: 91396 (NSThread: 0x6b298f0>{name = (null), num = 4})  
 after execution: 99999 (NSThread: 0x6a288a0>{name = (null), num = 5})  
 after execution: 99999 (NSThread: 0x6b2a6f0>{name = (null), num = 6})  

并不是每个线程的value都是99999。这是因为现在的代码并不是线程安全的。

所谓线程安全就是代码运行在多线程环境下和运行在单线程环境下是一样的。

是什么导致了这个行为呢? 正如前面所说的每个线程都有其自己的IP和堆栈,但却共享堆(heap)。例子中的FooClass是创建在堆上的,所有线程都可以使用。下图展示了两个线程在执行doIt方法时的冲突: :

Thread 1和Thread 2正在不同的位置执行。doIt()并没有对多线程的执行进行保护,它的实现是非线程安全的。

一个将doIt()变为线程安全的方式是在其函数体外使用如下编译指示符(directive):

新的代码如下所示:
- (void)doIt {  
   @synchronized(self) {  
     self.value = 0;   
     for (int i = 0; i < 100000; ++i) {  
       self.value = i;  
     }  
     NSLog(@"after execution: %d (%@)", self.value, [NSThread currentThread]);       
   }  
 } 

使用 @synchronized指示符 , 每个线程会在doIt()互斥地使用self。不过因为目前的代码中@synchronized包住了整个函数体,并不能达到并行执行的效果。

另一种同步访问机制是使用GCD: Grand Central Dispatch (GCD) .

 4) 如何识别非线程安全的代码 (How to identify not thread safe code)


上面例子太过于简单了。现实中,花了时间写好的代码,常常遇到死锁、崩溃,或者一些无法复现的问题。总之和期望的行为不一样。

线程问题的主因是共享或全局状态(state)数据。多个对象访问一个全局变量,或者在堆中分享了共同对象,再或者向共同的存储空间写入数据。在前面例子中所共享的状态是self, 对应的访问也就是 self.value。例子中所展示要比实际上的情况简单太多了,事实上确定使用的共享或全局状态(share or global state)并不容易。

解决方案就是写了一个工具,由多线程调用的函数来识别。下面是这个工具的核心概念。

工具主要包含了四个类: 
MultiThreadingAnalysis的实例用于记录一个线程对方法的调用,   ThreadingTrace类和 MethodExecution类用来输出 MultiThreadingAnalysis整理的分析结果 MultiThreadingAnalysisHook类则用于hook到对象并追踪它被调用的所有方法。




MultiThreadingAnalysis类提供两个方法 :
  • recordCallToMethod:ofClass:onThread: 记录某个方法在某个线程上被调用了。
  • threadingTraceOfLastApplicationRun 需要在分析完成后调用。
 @interface MultiThreadingAnalysis : NSObject  
   
      - (void)recordCallToMethod:(NSString*)methodName  
                ofClass:(NSString*)className  
               onThread:(NSString*)threadID;  
            
      - (ThreadingTrace*) threadingTraceOfLastApplicationRun;  
            
 @end  


分析结果由 ThreadingTrace来处理 . 它包含了一组 MethodExecution实例,每一个都表示了一个线程对一个方法的调用 :
 /*  
  * An instance of this class captures  
  * which methods of which classes have been  
  * called on which threads.  
  */  
 @interface ThreadingTrace : NSObject  
      /*  
       * Set of MethodExecution  
       */  
      @property (nonatomic, readonly) NSSet *methodExecutions;  
      - (void)addMethodExecution:(MethodExecution*)methodExec;  
 @end  
   
 /*  
  * An instance of this class represents a call  
  * to a method of a specific class on a thread  
  * with a specific threadID.  
  */  
 @interface MethodExecution : NSObject  
      @property (nonatomic, retain) NSString *methodName;  
      @property (nonatomic, retain) NSString *className;  
      @property (nonatomic, retain) NSString *threadID;  
 @end  

为了尽可能方法地记录方法的调用,我使用了NSProxy来hook对一个对象所有方法的调用。 MultiThreadingAnalysisHook类继承自 NSProxy,并在 forwardInvocation:  方法解析对target对象的调用. 在重定位到target对象前,会先使用一个 MultiThreadingAnalysis实例来记录下这次调用。
 
@interface MultiThreadingAnalysisHook : NSProxy  
      @property (nonatomic, retain) id target;  
      @property (nonatomic, retain) MultiThreadingAnalysis *analysis;  
 @end  
   
 @implementation MultiThreadingAnalysisHook  
   
 -(void)forwardInvocation:(NSInvocation*)anInvocation {  
     
   [self.analysis recordCallToMethod:NSStringFromSelector([anInvocation selector])  
                    ofClass:NSStringFromClass([self.target class])  
                onThread:[NSString stringWithFormat:@"%d", [NSThread currentThread]]];  
     
   [anInvocation invokeWithTarget:self.target];  
 }  
 @end

现在就可以使用了。在你要分析的类中创建一个私有方法 _withThreadingAnalysis  。 这个方法要创建一个 MultiThreadingAnalysisHook实例并且将target指到self。在自行指定的初始化函数中调用 _withThreadingAnalysis并返回其结果(HOOK的动作)。这样就达到使用 MultiThreadingAnalysisHook实例将原本对象的self封装起来,并可以记录所有外部对象的调用
 
@implementation YourClass  
   
 - (id)init {  
      //... do init stuff here  
      return [self _withThreadingAnalysis];  
 }  
   
 - (id)_withThreadingAnalysis {  
   MultiThreadingAnalysisHook *hook =   
     [[MultiThreadingAnalysisHook alloc] init];  
   hook.target = self;  
   return hook;  
 }  
 @end


此后就可以调用 MultiThreadingAnalysis   的 threadingTraceOfLastApplicationRun方法获取分析结果。最简单地输出到文本文件,结果如下:

begin threading analysis for class FooClass
   method doIt (_MultiThreadAccess_)
   method init (_SingleThreadAccess_)  

如果某个方法被多线程调用(标注为 _MultiThreadAccess_), 你可以看到更多详细信息。

转载请注明出处: http://blog.csdn.net/horkychen


目录
相关文章
|
1月前
|
存储 运维 安全
iOS加固原理与常见措施:保护移动应用程序安全的利器
iOS加固原理与常见措施:保护移动应用程序安全的利器
28 0
|
1月前
|
JSON JavaScript 安全
iOS应用程序数据保护:如何保护iOS应用程序中的图片、资源和敏感数据
iOS应用程序数据保护:如何保护iOS应用程序中的图片、资源和敏感数据
24 1
|
2月前
|
iOS开发 开发者
苹果iOS App Store上架操作流程详解:从开发者账号到应用发布
很多开发者在开发完iOS APP、进行内测后,下一步就面临上架App Store,不过也有很多同学对APP上架App Store的流程不太了解,下面我们来说一下iOS APP上架App Store的具体流程,如有未涉及到的部分,大家可以及时咨询,共同探讨。
|
2月前
|
开发者 iOS开发
iOS应用上架详细图文教程(上)
App Store作为苹果官方的应用商店,审核严格周期长一直让用户头疼不已,很多app都“死”在了审核这一关,那我们就要放弃iOS用户了吗?当然不是!本期我们从iOS app上架流程开始梳理,详细了解下iOS app上架的那些事。
|
2月前
|
Swift iOS开发 开发者
iOS 应用上架流程详解
iOS 应用上架流程详解
|
2月前
|
Android开发 iOS开发 UED
appuploader   iOS 应用自动发布
appuploader   iOS 应用自动发布
|
4天前
|
存储 编解码 JSON
利用SwiftUI构建高效iOS天气应用
【4月更文挑战第21天】 在本文中,我们将深入探讨如何运用SwiftUI框架打造一个响应迅速且用户友好的iOS天气应用程序。我们将重点放在利用SwiftUI的声明式语法简化界面开发,并通过结合Core Location和Networking APIs实现实时天气数据的获取与展示。文章将详细阐述整个开发过程,包括API集成、数据模型设计、用户界面布局以及动态适配不同屏幕尺寸的策略。
|
1月前
|
iOS开发 开发者
iOS移动应用程序的备案与SHA-1值查看
iOS移动应用程序的备案与SHA-1值查看
36 2
|
1月前
|
安全 数据安全/隐私保护 虚拟化
iOS应用加固方案解析:ipa加固安全技术全面评测
iOS应用加固方案解析:ipa加固安全技术全面评测
37 3
|
1月前
|
运维 监控 安全
应用研发平台EMAS常见问题之sophix ios flutter热更新如何解决
应用研发平台EMAS(Enterprise Mobile Application Service)是阿里云提供的一个全栈移动应用开发平台,集成了应用开发、测试、部署、监控和运营服务;本合集旨在总结EMAS产品在应用开发和运维过程中的常见问题及解决方案,助力开发者和企业高效解决技术难题,加速移动应用的上线和稳定运行。
77 0