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


目录
相关文章
|
26天前
|
算法 计算机视觉 iOS开发
iOS 实时图像处理技术:使用 Core Image 和 Metal 进行高效滤镜应用
【4月更文挑战第8天】 在移动设备上实现高效的图像处理功能是现代应用程序开发中的一个关键需求。苹果的iOS平台提供了Core Image和Metal两大技术,它们为开发者提供了强大的工具来实现复杂的图像处理任务。本文将探讨如何使用Core Image进行基础图像处理,并结合Metal的性能优势,开发出一个自定义的实时图像滤镜。我们将通过创建一个能够动态调整参数并且具有实时反馈效果的滤镜来演示这一过程。
|
26天前
|
算法 计算机视觉 iOS开发
iOS 实时图像处理技术:Core Image 框架的应用
【4月更文挑战第8天】 在移动设备上实现高效的图像处理功能,对于提升用户体验和扩展应用程序能力至关重要。苹果公司的iOS平台提供了强大的Core Image框架,它允许开发者以高效和直观的方式执行复杂的图像处理任务。本文将深入探讨Core Image框架的关键特性,并通过实例演示如何在iOS应用中集成实时图像处理功能,不仅提高性能,同时保持了电池寿命的优化。我们将重点讨论面部识别、滤镜应用和性能优化等关键技术点,为读者提供一份全面的iOS图像处理指南。
|
2月前
|
存储 运维 安全
iOS加固原理与常见措施:保护移动应用程序安全的利器
iOS加固原理与常见措施:保护移动应用程序安全的利器
29 0
|
2月前
|
JSON JavaScript 安全
iOS应用程序数据保护:如何保护iOS应用程序中的图片、资源和敏感数据
iOS应用程序数据保护:如何保护iOS应用程序中的图片、资源和敏感数据
24 1
|
3月前
|
iOS开发 开发者
苹果iOS App Store上架操作流程详解:从开发者账号到应用发布
很多开发者在开发完iOS APP、进行内测后,下一步就面临上架App Store,不过也有很多同学对APP上架App Store的流程不太了解,下面我们来说一下iOS APP上架App Store的具体流程,如有未涉及到的部分,大家可以及时咨询,共同探讨。
|
4天前
|
存储 Swift iOS开发
使用Swift开发一个简单的iOS应用的详细步骤。
使用Swift开发iOS应用的步骤包括:创建Xcode项目,设计界面(Storyboard或代码),定义数据模型,实现业务逻辑,连接界面和逻辑,处理数据存储(如Core Data),添加网络请求(必要时),调试与测试,根据测试结果优化改进,最后提交至App Store或其它平台发布。
13 0
|
4天前
|
安全 Swift iOS开发
【Swift 开发专栏】Swift 与 UIKit:构建 iOS 应用界面
【4月更文挑战第30天】本文探讨了Swift和UIKit在构建iOS应用界面的关键技术和实践方法。Swift的简洁语法、类型安全和高效编程模型,加上与UIKit的紧密集成,使开发者能便捷地创建用户界面。UIKit提供视图、控制器、布局、动画和事件处理等功能,支持灵活的界面设计。实践中,遵循设计原则,合理组织视图层次,运用布局和动画,以及实现响应式设计,能提升界面质量和用户体验。文章通过登录、列表和详情界面的实际案例展示了Swift与UIKit的结合应用。
|
4天前
|
存储 安全 Swift
【Swift 开发专栏】使用 Swift 开发一个简单的 iOS 应用
【4月更文挑战第30天】本文介绍了使用 Swift 开发简单 iOS 待办事项应用的步骤。首先,阐述了 iOS 开发的吸引力及 Swift 语言的优势。接着,详细说明了应用的需求和设计,包括添加、查看和删除待办事项的功能。开发步骤包括创建项目、界面搭建、数据存储、功能实现,并提供了相关代码示例。最后,强调了实际开发中需注意的细节和优化,旨在帮助初学者掌握 Swift 和 iOS 开发基础。
|
11天前
|
搜索推荐 API iOS开发
利用SwiftUI构建动态iOS天气应用
【4月更文挑战第23天】 在本文中,我们将探讨如何使用SwiftUI框架实现一个动态的iOS天气应用程序。将重点介绍如何通过集成第三方天气API、设计响应式用户界面以及应用数据绑定技术来增强用户体验。我们的目标是创建一个能够根据当前位置实时更新天气信息的应用,并确保其界面简洁、易用且美观。
10 0
|
13天前
|
存储 编解码 JSON
利用SwiftUI构建高效iOS天气应用
【4月更文挑战第21天】 在本文中,我们将深入探讨如何运用SwiftUI框架打造一个响应迅速且用户友好的iOS天气应用程序。我们将重点放在利用SwiftUI的声明式语法简化界面开发,并通过结合Core Location和Networking APIs实现实时天气数据的获取与展示。文章将详细阐述整个开发过程,包括API集成、数据模型设计、用户界面布局以及动态适配不同屏幕尺寸的策略。