iOS開發(fā)常用線程安全鎖
正文
多線程開發(fā),就會有資源搶占的情況,導致出現(xiàn)我們意想不到的數(shù)據(jù)問題,我們就需要對數(shù)據(jù)進行加鎖,已保證線程安全.
鎖主要分為兩大類自旋鎖和互斥鎖。
- 自旋鎖:自旋鎖已經(jīng)被別的執(zhí)行單元保持,調(diào)用者就一直循環(huán)在那里看是否該自旋鎖的保持者已經(jīng)釋放了鎖,因此是一種忙等待。自旋鎖避免了線程上下文切換的調(diào)度開銷,因此對于線程只會阻塞很短的時間是很高效的,但是對于比較長時間的阻塞也是比較消耗CPU的。(線程忙等)
- 互斥鎖:如果資源已經(jīng)被占用,資源申請者只能進入睡眠狀態(tài)。有上下文的切換(主動出讓時間片, 線程休眠, 等待下一次喚醒)、CPU的搶占、信號的發(fā)送等開銷。(線程閑等)
原子屬性
我們創(chuàng)建屬性一般都會設置屬性為非原子屬性noatomic, 因為原子屬性atomic會有額外的加鎖開銷,那如果我們創(chuàng)建屬性使用原子屬性atomic,它能保證property是線程安全的嗎?
#import "ViewController.h" @interface ViewController () @property (atomic ,assign) int count; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.count = 0; [self test_atomic]; } - (void)test_atomic { // self.count初始值是10 for (int i = 0; i < 10; i ++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ self.count ++; NSLog(@"%d",self.count); }); } } @end
從上面我們可以看到原子屬性atomic不能保證數(shù)據(jù)的線程安全.下面我們從源碼進行分析:在屬性的getter/setter方法調(diào)用的底層atomic和nonatomic有什么區(qū)別。先看看setter方法:objc_setProperty
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) { bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY); bool mutableCopy = (shouldCopy == MUTABLE_COPY); reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy); } void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset{ reallySetProperty(self, _cmd, newValue, offset, true, false, false); } void objc_setProperty_nonatomic(id self, SEL _cmd, id newValue, ptrdiff_t offset) { reallySetProperty(self, _cmd, newValue, offset, false, false, false); } void objc_setProperty_atomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset) { reallySetProperty(self, _cmd, newValue, offset, true, true, false); } void objc_setProperty_nonatomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset) { reallySetProperty(self, _cmd, newValue, offset, false, true, false); }
我們可以看到都是調(diào)用的reallySetProperty方法,atomic第五個參數(shù)為true,nonatomic為false, copy第六個參數(shù)為true, mutableCopy第七個參數(shù)為true.
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) { if (offset == 0) { object_setClass(self, newValue); return; } id oldValue; id *slot = (id*) ((char*)self + offset); if (copy) { newValue = [newValue copyWithZone:nil]; } else if (mutableCopy) { newValue = [newValue mutableCopyWithZone:nil]; } else { if (*slot == newValue) return; newValue = objc_retain(newValue); } if (!atomic) { oldValue = *slot; *slot = newValue; } else { spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); oldValue = *slot; *slot = newValue; slotlock.unlock(); } objc_release(oldValue); }
copy和mutableCopy使用copyWithZone進行新值的copy,其他使用objc_retain增加引用計數(shù).nonatomic直接進行賦值;atomic會使用spinlock_t在賦值之前加鎖,賦值之后解鎖. 我們再來看看getter方法:objc_getProperty
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) { if (offset == 0) { return object_getClass(self); } // Retain release world id *slot = (id*) ((char*)self + offset); if (!atomic) return *slot; // Atomic retain release world spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); id value = objc_retain(*slot); slotlock.unlock(); // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock. return objc_autoreleaseReturnValue(value); }
我們可以看到nonatomic直接返回,atomic在取值前加鎖,取值后解鎖,再返回值.
那么原子屬性atomic在getter/setter底層有加鎖解鎖操作,為什么不能保證線程安全的呢?
因為原子屬性atomic鎖住資源的范圍不夠大。在self.count --;的時候,既有getter也有setter,可能就出現(xiàn)當getter的時候還沒有return出去就被其它線程setter。
OSSpinLock - 自旋鎖
OSSpinLock 在iOS10之后被移除了。 被移除的原因是它有一個bug:優(yōu)先級反轉(zhuǎn)。
優(yōu)先級反轉(zhuǎn):當多個線程有優(yōu)先級的時候,有一個優(yōu)先級較低的線程先去訪問了資源,并是有了OSSpinLock對資源加鎖,又來一個優(yōu)先級較高的線程去訪問了這個資源,這個時候優(yōu)先級較高的線程就會一直占用cpu的資源,導致優(yōu)先級較低的線程沒辦法與較高的線程爭奪cpu的時間,最后導致最先被優(yōu)先級較低的線程鎖住的資源遲遲不能被釋放,從而造成優(yōu)先級反轉(zhuǎn)的bug。
所以 OSSpinLock使用限制:必須保證所有訪問同一資源的線程處于優(yōu)先級平等的時候,才可以使用。
OSSpinLock已被蘋果放棄了,大家也可以放棄它,蘋果設計了os_unfair_lock來代替OSSpinLock。
os_unfair_lock - 互斥鎖
iOS10之后開始支持,os_unfair_lock 在os庫中,使用之前需要導入頭文件<os/lock.h>。
#import "ViewController.h" #import <os/lock.h> @interface ViewController () @property (nonatomic ,assign) int count; @property (nonatomic ,assign) os_unfair_lock unfairLock; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.count = 0; self.unfairLock = OS_UNFAIR_LOCK_INIT; // 初始化鎖 } -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { for (int i = 0; i<10; i++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ os_unfair_lock_lock(&_unfairLock); // 加鎖 self.count ++; NSLog(@"%d",self.count); os_unfair_lock_unlock(&_unfairLock); // 解鎖 }); } } @end
NSLock - 互斥鎖
NSLock - Foundation框架內(nèi)部的??,使用起來非常方便,基于pthroad_mutex封裝而來,是一把互斥非遞歸鎖。因為OC的Foundation框架是非開源的,所以我們查看swift的Foundation框架,來查看其源碼實現(xiàn),原理是相同的
#if os(Windows) #elseif CYGWIN #else private typealias _MutexPointer = UnsafeMutablePointer<pthread_mutex_t> private typealias _RecursiveMutexPointer = UnsafeMutablePointer<pthread_mutex_t> private typealias _ConditionVariablePointer = UnsafeMutablePointer<pthread_cond_t> #endif open class NSLock: NSObject, NSLocking { internal var mutex = _MutexPointer.allocate(capacity: 1) #if os(macOS) || os(iOS) || os(Windows) private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1) private var timeoutMutex = _MutexPointer.allocate(capacity: 1) #endif public override init() { #if os(Windows) #else pthread_mutex_init(mutex, nil) #if os(macOS) || os(iOS) pthread_cond_init(timeoutCond, nil) pthread_mutex_init(timeoutMutex, nil) #endif #endif } deinit { #if os(Windows) #else pthread_mutex_destroy(mutex) #endif mutex.deinitialize(count: 1) mutex.deallocate() #if os(macOS) || os(iOS) || os(Windows) deallocateTimedLockData(cond: timeoutCond, mutex: timeoutMutex) #endif } open func lock() { #if os(Windows) #else pthread_mutex_lock(mutex) #endif } open func unlock() { #if os(Windows) #else pthread_mutex_unlock(mutex) #if os(macOS) || os(iOS) // Wakeup any threads waiting in lock(before:) pthread_mutex_lock(timeoutMutex) pthread_cond_broadcast(timeoutCond) pthread_mutex_unlock(timeoutMutex) #endif #endif } ... }
我們可以起內(nèi)部是對pthread_mutex_t的封裝
- 構造方法 init()就是調(diào)用了pthread的pthread_mutex_init(mutex, nil)方法
- 析構方法 deinit就是調(diào)用了pthread的pthread_mutex_destroy(mutex)方法
- 加鎖方法 lock()就是調(diào)用了pthread的pthread_mutex_lock(mutex)方法
- 解鎖方法 unlock()就是調(diào)用了pthread的pthread_mutex_unlock(mutex)方法
在pthread_mutex中可以通過pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))來設置鎖為遞歸鎖,這里并沒有設置,所以NSLock不是一把遞歸鎖!
NSCondition - 互斥鎖
我們通過查看swift foundation 源碼 可以看到其和NSLock類似,也是對pthread_mutex_t的封裝,相比于NSLock,NSCondition多了幾個API:
open func wait() { pthread_cond_wait(cond, mutex) } open func wait(until limit: Date) -> Bool { guard var timeout = timeSpecFrom(date: limit) else { return false } return pthread_cond_timedwait(cond, mutex, &timeout) == 0 } open func signal() { pthread_cond_signal(cond) } open func broadcast() { pthread_cond_broadcast(cond) }
- (void)wait 阻塞當前線程,使線程進入休眠,等待喚醒信號。調(diào)用前必須已加鎖。
- (void)waitUntilDate 阻塞當前線程,使線程進入休眠,等待喚醒信號或者超時。調(diào)用前必須已加鎖。
- (void)signal 喚醒一個正在休眠的線程,如果要喚醒多個,需要調(diào)用多次。如果沒有線程在等待,則什么也不做。調(diào)用前必須已加鎖。
- (void)broadcast 喚醒所有在等待的線程。如果沒有線程在等待,則什么也不做。調(diào)用前必須已加鎖。
#import "ViewController.h" @interface ViewController () @property (nonatomic ,assign) int count; @property (nonatomic ,strong) NSCondition *iCondition; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.count = 0; self.iCondition = [[NSCondition alloc] init]; // 初始化鎖 } -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [self nscondition_test]; } #pragma mark -- NSCondition - (void)nscondition_test { // 生產(chǎn) for (int i = 0; i < 50; i ++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self my_production]; }); } // 消費 for (int i = 0; i < 100; i ++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self my_consumption]; }); } } - (void)my_production { [self.iCondition lock]; self.count ++; NSLog(@"生產(chǎn)了一個產(chǎn)品,現(xiàn)有產(chǎn)品 : %d個",self.count); [self.iCondition signal]; // 喚醒一個wait正在休眠的線程 [self.iCondition unlock]; } - (void)my_consumption { [self.iCondition lock]; while (self.count == 0) { // 這里使用 if 會出現(xiàn)現(xiàn)有產(chǎn)品是負數(shù)的情況 [self.iCondition wait]; // 阻塞當前線程,使線程進入休眠,等待喚醒信號signal } self.count --; NSLog(@"消費了一個產(chǎn)品,現(xiàn)有產(chǎn)品: %d個",self.count); [self.iCondition unlock]; } @end
注意??:pthread_mutex 存在虛假喚醒的情況,一個signl喚醒多個wait,不是預期的signal : wait = 1:1效果。 在編碼過程中可以通過while條件判斷,使被喚醒的線程,陷入while循環(huán)中,從而解決此問題。
NSConditionLock - 互斥鎖
NSConditionLock是基于NSCondition的封裝。目的是讓NSConditionLock自帶條件探測
open class NSConditionLock : NSObject, NSLocking { internal var _cond = NSCondition() ...... open func lock(whenCondition condition: Int) { let _ = lock(whenCondition: condition, before: Date.distantFuture) } open func `try`() -> Bool { return lock(before: Date.distantPast) } open func tryLock(whenCondition condition: Int) -> Bool { return lock(whenCondition: condition, before: Date.distantPast) } open func unlock(withCondition condition: Int) { _cond.lock() _thread = nil _value = condition _cond.broadcast() _cond.unlock() } ... }
#import "ViewController.h" @interface ViewController () @property (nonatomic ,strong) NSConditionLock *iConditionLock; @end @implementation ViewController -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [self conditonLock_test]; } #pragma mark -- NSConditionLock - (void)conditonLock_test { self.iConditionLock = [[NSConditionLock alloc] initWithCondition:3]; dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self.iConditionLock lockWhenCondition:3]; NSLog(@"1"); [self.iConditionLock unlockWithCondition:2]; }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self.iConditionLock lockWhenCondition:2]; NSLog(@"2"); [self.iConditionLock unlockWithCondition:1]; }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self.iConditionLock lockWhenCondition:1]; NSLog(@"3"); [self.iConditionLock unlockWithCondition:0]; }); } @end // 線程任務的執(zhí)行順序:1 2 3
NSConditionLock能夠達到控制線程執(zhí)行任務順序的目的。
NSRecursiveLock
遞歸鎖:同一時刻只能被一條線程所擁有。 NSRecursiveLock是基于pthread的封裝,并設置了遞歸屬性。
open class NSRecursiveLock: NSObject, NSLocking { internal var mutex = _RecursiveMutexPointer.allocate(capacity: 1) #if os(macOS) || os(iOS) || os(Windows) private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1) private var timeoutMutex = _MutexPointer.allocate(capacity: 1) #endif public override init() { super.init() #if CYGWIN var attrib : pthread_mutexattr_t? = nil #else var attrib = pthread_mutexattr_t() #endif withUnsafeMutablePointer(to: &attrib) { attrs in pthread_mutexattr_init(attrs) // 設置遞歸屬性 pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE)) pthread_mutex_init(mutex, attrs) } pthread_cond_init(timeoutCond, nil) }
NSConditionLock是一把遞歸鎖,可遞歸加鎖解鎖(可適用于遞歸函數(shù))
通過PTHREAD_MUTEX_RECURSIVE來設置鎖為遞歸鎖。當鎖為遞歸鎖的時候,它的使用場景為單個線程中的遞歸調(diào)用。
#import "ViewController.h" @interface ViewController () @property (nonatomic ,assign) int count; @property (nonatomic ,strong) NSRecursiveLock *iRecursiveLock; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.count = 0; self.iRecursiveLock = [[NSRecursiveLock alloc] init]; // 初始化鎖 [self recursiveTest]; // 遞歸鎖案例 } -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { for (int i = 0; i<10; i++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self recursiveLock_test]; }); } } #pragma mark -- NSRecursiveLock -(void)recursiveLock_test { [self.iRecursiveLock lock]; self.count ++; NSLog(@"%d",self.count); [self.iRecursiveLock unlock]; } - (void)recursiveTest { dispatch_async(dispatch_get_global_queue(0, 0), ^{ static void (^recursiveMethod)(int); recursiveMethod = ^(int value){ if (value > 0) { [self.iRecursiveLock lock]; NSLog(@"%d",value); recursiveMethod(value - 1); [self.iRecursiveLock unlock]; } }; recursiveMethod(10); }); } @end
如果在不同線程進行遞歸調(diào)用的話,會出現(xiàn)問題,把recursiveTest方法放到for循環(huán)里
- (void)recursiveTest { for (int i = 0; i < 5; i++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ static void (^recursiveMethod)(int); recursiveMethod = ^(int value){ if (value > 0) { [self.iRecursiveLock lock]; NSLog(@"%d",value); recursiveMethod(value - 1); [self.iRecursiveLock unlock]; } }; recursiveMethod(10); }); } }
此時代碼會因為子線程相互等待資源而造成線程死鎖。
@synchronized
@synchronized不管你幾條線程,不管你是否遞歸調(diào)用,它都支持,是我們最常用的一把鎖,雖然都在詬病其性能問題,可是在真機條件下測試其性能,和其他鎖并沒有那么明顯的差別。
#import "ViewController.h" @interface ViewController () @property (nonatomic ,assign) int count; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.count = 0; [self synchronized_test]; // synchronized案例 } - (void)synchronized_test { for (int i=0; i<5; i++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ static void (^recursiveMethod)(int); recursiveMethod = ^(int value){ if (value > 0) { @synchronized(self) { NSLog(@"%d",value); recursiveMethod(value - 1); } } }; recursiveMethod(10); }); } } @end
@synchronized(obj)指令使用的obj為該鎖的唯一標識,只有當標識相同時,才為滿足互斥。, @synchronized還是個遞歸可重入鎖,如下代碼所示:
NSObject *obj = [[NSObject alloc] init]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{ @synchronized(obj){ NSLog(@"1開始"); @synchronized (obj) { NSLog(@"2開始"); @synchronized (obj) { NSLog(@"3"); } NSLog(@"2完成"); } NSLog(@"1結束"); } });
@synchronized是個遞歸互斥鎖,同一個線程可以重復獲得這個鎖并進入執(zhí)行執(zhí)行塊里面的代碼而不會導致死鎖。
@synchronized的優(yōu)點:不需要在代碼中顯式的創(chuàng)建鎖對象,便可以實現(xiàn)鎖的機制;遞歸互斥,同一個線程可以重復進入而不導致死鎖。
@synchronized的缺點:效率低(在真機上不見得效率那么低)。@synchronized塊會隱式的添加一個異常處理例程來保護代碼,該處理例程會在異常拋出的時候自動的釋放互斥鎖,這會增加額外的開銷。同時為了實現(xiàn)遞歸互斥可重入,底層使用的是遞歸鎖加上復雜的業(yè)務邏輯,也增加了不少的消耗。
@synchronized加鎖需要一個對象參數(shù),在選著對象參數(shù)的時候要特別注意不能讓對象參數(shù)為nil,否則加鎖無效。
Semaphore信號量
同樣的信號量也可以解決線程安全問題,相關內(nèi)容請查閱GCD篇章,主要是控制并發(fā)數(shù)量,來實現(xiàn)線程安全
#pragma mark -- dispatch_semaphore_t - (void)dispatch_semaphore_t_test { dispatch_semaphore_t sem = dispatch_semaphore_create(0); dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"任務1"); dispatch_semaphore_signal(sem); }); dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"任務2"); dispatch_semaphore_signal(sem); }); dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"任務3"); }); }
pthread_mutex
純C的鎖,需要我們自己進行對象的內(nèi)存管理,前面有些鎖就是對齊進行的封裝.
#pragma mark -- pthread_mutex - (void)pthread_mutex_test { //非遞歸加鎖 pthread_mutex_t lock0; pthread_mutex_init(&lock0, NULL); pthread_mutex_lock(&lock0); // 鎖住的資源... pthread_mutex_unlock(&lock0); pthread_mutex_destroy(&lock0); // c對象,需要自己釋放資源 //遞歸加鎖 pthread_mutex_t lock; pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // 設置遞歸屬性 pthread_mutex_init(&lock, &attr); pthread_mutexattr_destroy(&attr); pthread_mutex_lock(&lock); // 鎖住的資源... pthread_mutex_unlock(&lock); pthread_mutex_destroy(&lock); // c對象,需要自己釋放資源 }
讀寫鎖
讀寫鎖實際是一種特殊的自旋鎖,它把對共享資源的訪問者劃分成讀者和寫者,讀者只對共享資源進行讀訪問,寫者則需要對共享資源進行寫操作。這種鎖相對于自旋鎖而言,能提高并發(fā)性,因為在多處理器系統(tǒng)中,它允許同時有多個讀者來訪問共享資源,最大可能的讀者數(shù)為實際的邏輯CPU數(shù)。寫者是排他性的,一個讀寫鎖同時只能有一個寫者或多個讀者(與CPU數(shù)相關),但不能同時既有讀者又有寫者。在讀寫鎖保持期間也是搶占失效的。
如果讀寫鎖當前沒有讀者,也沒有寫者,那么寫者可以立刻獲得讀寫鎖,否則它必須自旋在那里,直到?jīng)]有任何寫者或讀者。如果讀寫鎖沒有寫者,那么讀者可以立即獲得該讀寫鎖,否則讀者必須自旋在那里,直到寫者釋放該讀寫鎖。
讀寫鎖可以實現(xiàn)多讀單寫功能(讀讀并發(fā)、讀寫互斥、寫寫互斥) 我們通過GCD的柵欄函數(shù)實現(xiàn)的一個簡單讀寫鎖案例:
#import "ViewController.h" @interface ViewController () @property (nonatomic ,strong) dispatch_queue_t iQueue; @property (nonatomic ,strong) NSMutableDictionary *dataDic; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.iQueue = dispatch_queue_create("AnAn", DISPATCH_QUEUE_CONCURRENT); self.dataDic = [NSMutableDictionary new]; [self my_write: @"我是寫的東西"]; } - (void)test { for (int i = 0; i < 10; i ++) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self my_read]; }); } } #pragma mark -- 讀寫鎖 - (NSString *)my_read { // 異步讀取 __block NSString *ret; dispatch_sync(self.iQueue, ^{ // 讀取的代碼 ret = self.dataDic[@"name"]; }); NSLog(@"%@",ret); return ret; } - (void)my_write: (NSString *)name { // 寫操作 dispatch_barrier_async(self.iQueue, ^{ [self.dataDic setObject:name forKey:@"name"]; }); }
以上就是iOS開發(fā)常用線程安全鎖的詳細內(nèi)容,更多關于iOS線程安全鎖的資料請關注腳本之家其它相關文章!
相關文章
iOS基于UIScrollView實現(xiàn)滑動引導頁
這篇文章主要為大家詳細介紹了iOS基于UIScrollView實現(xiàn)滑動引導頁的相關資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-01-01iOS開發(fā)tips-UINavigationBar的切換效果
這篇文章主要為大家詳細介紹了iOS開發(fā)tips-UINavigationBar的切換效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-11-11iOS開發(fā)TableView網(wǎng)絡請求及展示預加載實現(xiàn)示例
這篇文章主要為大家介紹了iOS開發(fā)TableView網(wǎng)絡請求及展示預加載實現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-07-07