/
content.json
1 lines (1 loc) · 97.9 KB
/
content.json
1
[{"title":"『钻』研iOS 之深入理解NSRunLoop","date":"2017-02-12T05:43:41.000Z","path":"2017/02/12/「钻」研iOS 之深入理解NSRunLoop/","text":"差不多2年前,我对NSRunLoop有过应用层面上的理解,现在我们通过苹果开源在github上的swift-corelibs-foundation来深入了解下苹果是如何实现的这个CFRunLoop(NSRunLoop是对CFRunLoop的封装)。当然,还是2年前的那句话:NSRunLoop其英文释义一样,是运行一个无限循环,她是跟线程一起存在的。在主线程中NSRunLoop是默认启动的;在多线程中NSRunLoop默认不是启动的,需要开发者手动运行才能启动。 RunLoop的概念RunLoop本质上是一个Event Loop,实现的是一个do-while循环,主要用来处理事件消息。苹果设计的高明之处:是进入do-while循环之后不会导致死循环,因为mach_port的存在,会让这个RunLoop在某个事件处睡眠,事件循环就暂停,再在需要的时候通过mach_port唤醒RunLoop,事件循环就会继续处理。 __CFRunLoop & __CFRunLoopMode源码中的结构体定义如下:123456789101112131415161718192021struct __CFRunLoop { ... pthread_mutex_t _lock; //对象锁,线程保护 ... CFMutableSetRef _commonModes; //这个Set主要CommonModes类型的名字,能保证不重复名字的mode CFMutableSetRef _commonModeItems; //commonModes里可以插入不同Mode Items(可包含Source、Timer和Observer类型) CFRunLoopModeRef _currentMode; //当前执行的mode CFMutableSetRef _modes; //runLoop的modes,每次执行循环只能执行其中的一个 ...};struct __CFRunLoopMode { ... pthread_mutex_t _lock; /* must have the run loop locked before locking this */ //对象锁,保证线程安全 ... CFMutableSetRef _sources0; //source0类型的CFRunLoopSource的set CFMutableSetRef _sources1; //source1类型的CFRunLoopSource的set CFMutableArrayRef _observers; //observer数组 CFMutableArrayRef _timers; //timer数组 ...}; 由结构体定义可知__CFRunLoop内部有_commonModes、_commonModeItems和_modes2个SET集合: commonModes(SET类型)只存储Mode的别名; commonModeItems(SET类型)存储的__CFRunLoopMode的结构体; __CFRunLoopMode内部定义了这个4个集合类的实例: source0(SET类型):用来存储__CFRunLoopSource的结构体对象; source1(SET类型):用来存储__CFRunLoopSource的结构体对象,该对象在runLoop睡眠时被唤醒; timer(Array类型):用来存储__CFRunLoopTimer的结构体对象, observer(Array类型):用来存储__CFRunLoopObserver的结构体对象; modes:runLoop执行的mode,每次循环只能执行其中的一个Mode。 说明一个__CFRunLoop中可以插入多个Mode,每个Mode又可以包含多个source事件、多个timer实例和多个观察者(observer)对象,Source、Timer和Observer统称Mode Item。 至于source为何选用set类型,而timer与observer选用的array类型?猜测:SET集合既能保证数据的唯一性(hash值比较),又能快速索引(比如NSObject cancel延时事件,需要快速准确得找到cancel的source这种场景)。 __CFRunLoopSource & __CFRunLoopTimer & __CFRunLoopObserver12345678910111213141516171819202122232425262728293031323334353637struct __CFRunLoopSource { CFRuntimeBase _base; uint32_t _bits; pthread_mutex_t _lock; CFIndex _order; /* immutable */ CFMutableBagRef _runLoops; union { //联合体能保证在同一个时间内不会同时使用version0和version1,2个数据结构体中只有一个会存在,符合runLoop的设计 CFRunLoopSourceContext version0; /* immutable, except invalidation */ CFRunLoopSourceContext1 version1; /* immutable, except invalidation */ } _context;};typedef struct { CFIndex version; void * info; const void *(*retain)(const void *info); void (*release)(const void *info); CFStringRef (*copyDescription)(const void *info); Boolean (*equal)(const void *info1, const void *info2); CFHashCode (*hash)(const void *info); //定时处理,适用`performSelector:withObject:afterDelay:` void (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode); //取消处理,适用`cancelPreviousPerformRequestsWithTarget:selector:object:` void (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode); void (*perform)(void *info);} CFRunLoopSourceContext; //iOS的performSelector系列方法会用这个Context存储函数指针,并且可以取消typedef struct { CFIndex version; void * info; const void *(*retain)(const void *info); void (*release)(const void *info); CFStringRef (*copyDescription)(const void *info); Boolean (*equal)(const void *info1, const void *info2); CFHashCode (*hash)(const void *info); mach_port_t (*getPort)(void *info); void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);} CFRunLoopSourceContext1; __CFRunLoopSource是事件产生的地方,共在2种类型:Source0和Source1,struct内部通过一个union保证同一个Source对象不会同时包含version0和version1; Source0只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(rlms)方法将这个Source标记为待处理,然后手动调用CFRunLoopWakeUp(rl)方法来唤醒RunLoop,让其处理这个事件。 Source1包含了一个mach_port和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种类型的Source能主动唤醒RunLoop的线程。1234567891011121314struct __CFRunLoopTimer { CFRuntimeBase _base; uint16_t _bits; pthread_mutex_t _lock; CFRunLoopRef _runLoop; CFMutableSetRef _rlModes; CFAbsoluteTime _nextFireDate; CFTimeInterval _interval; /* immutable */ CFTimeInterval _tolerance; /* mutable */ uint64_t _fireTSR; /* TSR units */ CFIndex _order; /* immutable */ CFRunLoopTimerCallBack _callout; /* immutable */ CFRunLoopTimerContext _context; /* immutable, except invalidation */}; __CFRunLoopTimer:是基于时间的触发器,它和NSTimer是toll-free bridged的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到RunLoop时,RunLoop会注册对应的时间点,当时间点到达时,RunLoop会被唤醒以执行那个回调。12345678910struct __CFRunLoopObserver { CFRuntimeBase _base; pthread_mutex_t _lock; CFRunLoopRef _runLoop; CFIndex _rlCount; CFOptionFlags _activities; /* immutable */ CFIndex _order; /* immutable */ CFRunLoopObserverCallBack _callout; /* immutable */ CFRunLoopObserverContext _context; /* immutable, except invalidation */}; __CFRunLoopObserver:是观察者,每个Observer都包含了一个回调(函数指针),当 RunLoop的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:12345678typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // 即将进入Loop kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠 kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒 kCFRunLoopExit = (1UL << 7), // 即将退出Loop}; RunLoop的设计CFRunLoop的设计是一个循环处理多个类型的事件处理(Timers,Source、Observer等)模型。工作流程如下图: RunLoop中虽然包含了多个Mode,但一次循环只能执行其中一个Mode,如果要切换Mode,必须等上一次的循环结束。 苹果公开提供的Mode有两个:kCFRunLoopDefaultMode(NSDefaultRunLoopMode)和 UITrackingRunLoopMode,你可以用这两个ModeName来操作其对应的Mode。 注意:这里有个概念叫CommonModes:一个Mode可以将自己标记为”Common”属性(通过将其 ModeName添加到RunLoop的”commonModes”中)。每当RunLoop的内容发生变化时,RunLoop都会自动将_commonModeItems里的Source/Observer/Timer同步到具有”Common”标记的所有Mode里。我们可以通过NSRunLoopCommonModes关键字操作CommonModeItems,这个也是苹果开放的API。 我们可以通过addTimer:forMode:方法把NSTimer(NSTimer默认是在NSRunLoopDefaultMode里)加载到CommonModeItems里,保证NSTimer的触发在UITrackingRunLoopMode里也能触发。 RunLoop核心方法源码中的核心方法整理如下123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118void CFRunLoopRun(void) { /* DOES CALLOUT */ int32_t result; do { result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); CHECK_FOR_FORK(); } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);}SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ CHECK_FOR_FORK(); if (modeName == NULL || modeName == kCFRunLoopCommonModes || CFEqual(modeName, kCFRunLoopCommonModes)) { //如果运行到了CurrentMode是CommonModes,RunLoop就退出了。 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ CFLog(kCFLogLevelError, CFSTR(\"invalid mode '%@' provided to CFRunLoopRunSpecific - break on _CFRunLoopError_RunCalledWithInvalidMode to debug. This message will only appear once per execution.\"), modeName); _CFRunLoopError_RunCalledWithInvalidMode(); }); return kCFRunLoopRunFinished; } if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished; __CFRunLoopLock(rl); CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false); if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) { Boolean did = false; //这里的did是没写完么?永远是false if (currentMode) __CFRunLoopModeUnlock(currentMode); //这里返回的CFRunLoopMode是加锁的,所以在这里要进行一次解锁 __CFRunLoopUnlock(rl); return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished; } volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl); CFRunLoopModeRef previousMode = rl->_currentMode; rl->_currentMode = currentMode; int32_t result = kCFRunLoopRunFinished; if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); //runLoop循环处理方法开始 if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); __CFRunLoopModeUnlock(currentMode); __CFRunLoopPopPerRunData(rl, previousPerRun); rl->_currentMode = previousMode; __CFRunLoopUnlock(rl); return result;}/* rl, rlm are locked on entrance and exit *///这个方法相关长,精简出重要代码static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { ... do { __CFRunLoopUnsetIgnoreWakeUps(rl); //通知Observers,处理Timers if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); //通知Observers,处理Sources if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); //处理加入的Block __CFRunLoopDoBlocks(rl, rlm); //处理Source0的事件 Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); if (sourceHandledThisLoop) { __CFRunLoopDoBlocks(rl, rlm); } Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR); didDispatchPortLastTime = false; //通知Observers 即将睡眠 if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); __CFRunLoopSetSleeping(rl); ... __CFRunLoopSetIgnoreWakeUps(rl); // user callouts now OK again __CFRunLoopUnsetSleeping(rl); //通知Observer 睡眠唤醒 if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); CFRUNLOOP_WAKEUP_FOR_SOURCE(); // Despite the name, this works for windows handles as well CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort); if (rls) { mach_msg_header_t *reply = NULL; //处理Source1的事件 sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop; if (NULL != reply) { (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL); CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply); } } ... __CFRunLoopDoBlocks(rl, rlm); //睡眠方法唤醒以后处理加入了的Block事件 if (sourceHandledThisLoop && stopAfterHandle) { retVal = kCFRunLoopRunHandledSource; // } else if (timeout_context->termTSR < mach_absolute_time()) { retVal = kCFRunLoopRunTimedOut; //判断是否超时 } else if (__CFRunLoopIsStopped(rl)) { __CFRunLoopUnsetStopped(rl); retVal = kCFRunLoopRunStopped; } else if (rlm->_stopped) { rlm->_stopped = false; retVal = kCFRunLoopRunStopped; } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { retVal = kCFRunLoopRunFinished; } } while (0 == retVal); return retVal;} 线程安全CFRunLoop是线程安全的,CFRunLopp是纯C的API封装。从源代码定义的各个结构体对象中都会包含pthread_mutex的成员变量。pthread_mutex被实例成了递归锁,递归锁能保证在同一个线程里被多次调用不会造成锁等待的情况,但在多线程中能保证数据同步而存在锁等待的效果。123456789CF_INLINE void __CFRunLoopLockInit(pthread_mutex_t *lock) { pthread_mutexattr_t mattr; pthread_mutexattr_init(&mattr); pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_RECURSIVE); //初始化成递归锁 int32_t mret = pthread_mutex_init(lock, &mattr); pthread_mutexattr_destroy(&mattr); if (0 != mret) { }} RunLoop实现的功能广告:更多代码demo可以进入我的github进行下载查看运行结果,基本上每行关键代码都有详细的注释。本文中出现的代码都在这个项目下面,请结合项目代码阅读本文,效果更佳。 事件响应 & 手势识别 & AutoReleasePool & UI更新当我们启动一个app的时候,点击暂停线程我们会看到这样一个堆栈关系:先来看下主线程的po CFRunLoopGetCurrent(),整理以后如下:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129CFRunLoop{ wakeup port = 0x1e03, stopped = false, ignoreWakeUps = false, current mode = kCFRunLoopDefaultMode, common modes = { UITrackingRunLoopMode //CFString Mode名称 kCFRunLoopDefaultMode //CFString }, common mode items = { 0 : callout = PurpleEventSignalCallback 1 : ... 2 : callout = __handleHIDEventFetcherDrain //释放HIDEvent对象的callback(source0) 5 : ... 6 : callout = _wrapRunLoopWithAutoreleasePoolHandler //AutoReleasePool最高优先级处理 (observer) 7 : callout = _wrapRunLoopWithAutoreleasePoolHandler //AutoReleasePool最低优先级处理 (observer) 8 : callout = _afterCACommitHandler //监听CATransaction,刷新UI(observer) 9 : callout = _beforeCACommitHandler //监听CATransaction 10 : callout = _UIGestureRecognizerUpdateObserver //手势检测回调 11 : callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv 13 : callout = FBSSerialQueueRunLoopSourceHandler //Front Board Services 16 : callout = __handleEventQueue //用户事件回调 19 : ... 21 : callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_ 22 : callout = PurpleEventCallback }, modes = { 2 : CFRunLoopMode { name = UITrackingRunLoopMode, sources0 = { 0 : callout = PurpleEventSignalCallback 2 : callout = FBSSerialQueueRunLoopSourceHandler 4 : callout = __handleEventQueue 5 : callout = __handleHIDEventFetcherDrain }, sources1 = { 0 : callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_ 3 : ... 4 : ... 5 : callout = PurpleEventCallback 6 : ... }, observers = ( callout = _wrapRunLoopWithAutoreleasePoolHandler, callout = _UIGestureRecognizerUpdateObserver, callout = _beforeCACommitHandler, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv, callout = _afterCACommitHandler, callout = _wrapRunLoopWithAutoreleasePoolHandler ), timers = (null), }, 3 : CFRunLoopMode { name = GSEventReceiveRunLoopMode, sources0 = { callout = PurpleEventSignalCallback }, sources1 = { callout = PurpleEventCallback }, observers = (null), timers = (null), }, 4 : CFRunLoopMode { name = kCFRunLoopDefaultMode, sources0 = { 0 : callout = PurpleEventSignalCallback 2 : callout = FBSSerialQueueRunLoopSourceHandler 4 : callout = __handleEventQueue 5 : callout = __handleHIDEventFetcherDrain }, sources1 = { 0 : callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_ 3 : ... 4 : ... 5 : callout = PurpleEventCallback 6 : ... }, observers = ( callout = _wrapRunLoopWithAutoreleasePoolHandler, callout = _UIGestureRecognizerUpdateObserver, callout = _beforeCACommitHandler, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv, callout = _afterCACommitHandler, callout = _wrapRunLoopWithAutoreleasePoolHandler ), timers = (null), }, 5 : CFRunLoopMode { name = UIInitializationRunLoopMode, sources0 = { callout = FBSSerialQueueRunLoopSourceHandler }, sources1 = (null) observers = ( callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv ), timers = (null), }, 6 : CFRunLoopMode { name = kCFRunLoopCommonModes, sources0 = (null), sources1 = (null), observers = (null), timers = (null), }, }} 主线程默认创建的CFRunLoop包含了5个Mode类型: UITrackingRunLoopMode:该Mode能确保UIScrollView的滚动流畅性,UI滚动时切换到的是这个Mode,而NSTimer是在defaultMode里,存在NSTimer不触发的问题,所以我们才把NSTimer加到commonMode里以后在UITrackingRunLoopMode也会触发timer了。 GSEventReceiveRunLoopMode: kCFRunLoopDefaultMode:默认Mode类型,用户事件一般都会被加载在这个Mode的Source0处。 UIInitializationRunLoopMode:初始化app时的过渡Mode。 kCFRunLoopCommonModes:公共Mode类型,一般情况下是空的。 在CommonModeItems里注册了以下可识别的Mode Items:Observer类型:_wrapRunLoopWithAutoreleasePoolHandler: //AutoReleasePool最高优先级处理 (observer)_wrapRunLoopWithAutoreleasePoolHandler: //AutoReleasePool最低优先级处理 (observer)以上2个观察者与内存管理有关。_afterCACommitHandler: //监听CATransaction,刷新UI(observer)_beforeCACommitHandler: //监听CATransaction以上2个观察者有界面刷新有关。_UIGestureRecognizerUpdateObserver: //手势检测回调 (observer),手势变化时都会被这个观察者捕获。mach_msg_trap状态时也需要被RunLoop唤醒以后处理。 Source类型:_handleHIDEventFetcherDrain: //释放IOHIDEvent对象的callback(source0),所以有IOHIDEvent事件的位置(通常是唤醒RunLoop的位置)都会有这个回调方法。_handleEventQueue: //用户事件回调(source0),一般的addTarget: action: forControlEvents:方法都会加在source0,并由_handleEventQueue执行。 再看下事件点击线程6的po CFRunLoopGetCurrent(),整理以后如下:1234567891011121314151617181920212223242526272829303132333435CFRunLoop{ wakeup port = 0x3403, stopped = false, ignoreWakeUps = false, current mode = kCFRunLoopDefaultMode, common modes = { contents = \"kCFRunLoopDefaultMode\" }, common mode items = { callout = _UIEventFetcherTriggerHandOff callout = __IOHIDEventSystemClientAvailabilityCallback callout = __IOMIGMachPortPortCallback callout = __IOHIDEventSystemClientQueueCallback }, modes = { CFRunLoopMode { name = kCFRunLoopDefaultMode sources0 = { callout = _UIEventFetcherTriggerHandOff //标记UIEvent事件待处理的回调方法 } , sources1 = { callout = __IOHIDEventSystemClientQueueCallback //屏幕触摸事件回调 callout = __IOHIDEventSystemClientAvailabilityCallback //待研究 callout = __IOMIGMachPortPortCallback //待研究 }, observers = (null), timers = (null), }, }} 然后我们在Symbolic BreakPoint中添加一个__IOHIDEventSystemClientQueueCallback和_UIEventFetcherTriggerHandOff断点(该处理需要阅读者自行处理,属于Xcode的配置)。再触摸屏幕时,我们可以看到断点在以下2个堆栈关系图里:流程分析:当app启动默认会启动主线程的RunLoopM(M表示主线程),它在处理完一些事件以后进入mach_msg_trap状态,同时开启了一个事件处理线程6,在RunLoop6(6表示线程6)的source1添加了监听__IOHIDEventSystemClientQueueCallback方法,source0里添加了_UIEventFetcherTriggerHandOff方法,用户不触摸屏幕时该RunLoop也会进入mach_msg_trap状态。当用户触摸屏幕以后,事件线程6的RunLoop6最先被唤醒后执行source1里的__IOHIDEventSystemClientQueueCallback方法来唤醒主线程的RunLoopM,同时RunLoop6的source0里的_UIEventFetcherTriggerHandOff方法会把主线程的RunLoopM里的source0里的用户事件标记为待处理状态,紧接着唤醒的主线程RunLoopM会处理source0里的用户事件。 performSelector:object:afterDelay:performSelector延时系列方法也需要RunLoop处理,在内部会创建一个Timer计时来延时执行,所以RunLoop必须是在运行状态才成处理成功。如果不在主线程,需要开发者启动RunLoop来让方法生效。 参考资料:深入理解RunLoop –ibireme大神的深入专研精神真的令人倾佩。NSRunLoopEvent LoopMach(kernel)","tags":[{"name":"Objective-C","slug":"Objective-C","permalink":"http://www.dejohndong.com/tags/Objective-C/"},{"name":"iOS基础","slug":"iOS基础","permalink":"http://www.dejohndong.com/tags/iOS基础/"},{"name":"RunLoop","slug":"RunLoop","permalink":"http://www.dejohndong.com/tags/RunLoop/"},{"name":"iOS事件响应","slug":"iOS事件响应","permalink":"http://www.dejohndong.com/tags/iOS事件响应/"}]},{"title":"『钻』研iOS 之 内存管理(二)","date":"2017-02-06T12:00:29.000Z","path":"2017/02/06/「钻」研iOS 之内存管理2/","text":"上一讲中我把内存管理的基础、MRC与ARC作了自己的一些理解和经验分享,这一讲我会继续iOS内存管理方面的讲解。 Block的内存管理Block有3种内存类型:NSGlobalBlock(全局块):block内部没有引用任何外部变量的block是全局block(Global Block);NSStackBlock(栈内存块):block内部引用了block之外的外部变量的block是栈内存block(Stack Block);NSMallocBlock(堆内存块):block内部引用了block之外的外部变量的block并且被copy了一次是堆内存block(Malloc Block)。广告:更多代码demo可以进入我的github进行下载查看运行结果,基本上每行关键代码都有详细的注释。本文中出现的代码都在这个项目下面,请结合项目代码阅读本文,效果更佳。 先看Block内存变化的相关代码12345678910111213141516171819202122232425262728293031323334353637383940414243- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. //block内存变化示例 [self blockMemoryChangeExample]; //block关键字变化引用示例 //[self blockExample]; //block循环引用示例 //[self cycleReferenceBlockExample];}/** block内存变化示例 内存的变化顺序为{->[alloc]GlobalBlock、->[alloc]StackBlock->[copy]MallocBlock}, block最初被设定的内存类型为全局内存类型(无外部变量),当block内部出现外部变量时转换为栈内存类型,再当此栈内存block被外部持有(copy)操作时会变成堆内存类型。 */- (void)blockMemoryChangeExample { void (^ddkitBlockGlobal)() = ^(){ int b = 18; //没有引用外部变量的block是__NSGlobalBlock__ NSLog(@\"this is global block b is %d\", b); }; NSLog(@\"this block type is %@\", ddkitBlockGlobal); //block内部没有使用任何的外部变量,所以是__NSGlobalBlock__ ddkitBlockGlobal(); int a = 28; //使用__weak能阻止编译器对block进行copy操作,从而保证了block的内存类型是栈block(__NSStackBlock__) __weak void (^ddkitBlockStack)() = ^(){ int b = a; //使用外部变量的block是__NSStackBlock__ NSLog(@\"this is stack block, b is %d\", b); }; NSLog(@\"this block type is %@\", ddkitBlockStack); //block内部使用了的外部变量a,所以是__NSStackBlock__ ddkitBlockStack(); //block赋值操作 void (^blockMemoryType)(void) = ddkitBlockGlobal; //对全局内存block copy不会形成堆内存block,内存地址也没有发生改变 NSLog(@\"copy from ddkitBlockGlobal blockMemoryType is %@, ddkitBlockGlobal is %@\", blockMemoryType, ddkitBlockGlobal); blockMemoryType = ddkitBlockStack; //对栈内存block copy会形成堆内存block, 会产生新的地址 NSLog(@\"copy from ddkitBlockStack blockMemoryType is %@, ddkitBlockStack is %@\", blockMemoryType, ddkitBlockStack);} 运行结果如下: 总结分析:Block内存的变化流程图如下:我的总结:1.全局内存Block和栈内存Block都是系统自己进行内存管理的,与类的方法的内存管理一致;2.__weak显示调用可以阻止编译器对栈内存Block进行copy,所以ddkitBlockStack保留了栈内存类型b;3.Xcode编译器在ARC环境下给对象的默认关键字是__strong,所以blockMemoryType = ddkitBlockStack赋值形成了堆内存Block。 MRC环境下尝试Block的内存引用计数相关的示例代码12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849#pragma mark - MRC下测试block相关的retain、release、copy操作//全局内存Block尝试retain、copy、release等方法- (void)mrcGlobalBlockExample{ NSLog(@\"===== MRC GlobalBlock retainCount methods begin =====\"); void (^ddkitBlockGlobal)() = ^(){ int b = 18; //没有引用外部变量的block是__NSGlobalBlock__ NSLog(@\"this is global block b is %d\", b); }; ddkitBlockGlobal(); [ddkitBlockGlobal release]; //release操作对全局内存Block无效 NSLog(@\"ddkitBlockGlobal retainCount = %lu\", [ddkitBlockGlobal retainCount]); void (^ddkitBlockGlobalRetain)() = [ddkitBlockGlobal retain]; //尝试retain方法 NSLog(@\"ddkitBlockGlobal[%@] retainCount = %lu, ddkitBlockGlobalRetain[%@] retainCount = %lu\", ddkitBlockGlobal, [ddkitBlockGlobal retainCount], ddkitBlockGlobalRetain, [ddkitBlockGlobalRetain retainCount]); void (^ddkitBlockGlobalCopy)() = [ddkitBlockGlobal copy]; //尝试copy方法 NSLog(@\"ddkitBlockGlobal[%@] retainCount = %lu, ddkitBlockGlobalCopy[%@] retainCount = %lu\", ddkitBlockGlobal, [ddkitBlockGlobal retainCount], ddkitBlockGlobalCopy, [ddkitBlockGlobalCopy retainCount]); NSLog(@\"===== MRC GlobalBlock retainCount methods end =====\");}//栈内存Block尝试retain、copy、release等方法- (void)mrcStackBlockExample{ NSLog(@\"===== MRC StackBlock retainCount methods begin =====\"); int a = 10; void (^ddkitBlockGlobal)() = ^(){ int b = 28 + a; //引用外部变量的block是__NSStackBlock__ NSLog(@\"this is stack block b is %d\", b); }; ddkitBlockGlobal(); [ddkitBlockGlobal release]; //release操作对栈内存Block无效 NSLog(@\"ddkitBlockGlobal retainCount = %lu\", [ddkitBlockGlobal retainCount]); void (^ddkitBlockGlobalRetain)() = [ddkitBlockGlobal retain]; //尝试retain方法 NSLog(@\"ddkitBlockGlobal[%@] retainCount = %lu, ddkitBlockGlobalRetain[%@] retainCount = %lu\", ddkitBlockGlobal, [ddkitBlockGlobal retainCount], ddkitBlockGlobalRetain, [ddkitBlockGlobalRetain retainCount]); void (^ddkitBlockGlobalCopy)() = [ddkitBlockGlobal copy]; //尝试copy方法 NSLog(@\"ddkitBlockGlobal[%@] retainCount = %lu, ddkitBlockGlobalCopy[%@] retainCount = %lu\", ddkitBlockGlobal, [ddkitBlockGlobal retainCount], ddkitBlockGlobalCopy, [ddkitBlockGlobalCopy retainCount]); [ddkitBlockGlobalCopy release]; //release操作对堆内存Block有效,所以下一行注释掉代码出现崩溃。 //NSLog(@\"ddkitBlockGlobalCopy = %@\",ddkitBlockGlobalCopy); NSLog(@\"===== MRC StackBlock retainCount methods end =====\");} 运行结果如下: 总结分析:1.全局内存Block和栈内存Block的copy、release、retain方法都是无效的,指针地址和retainCount都不会发生变化。2.只有栈内存Block通过copy方法能变成堆内存Block(retain方法无效,retain方法对所有内存类型的block都起不到引用计数+1的作用),然后通过release方法可以释放堆内存Block。 blockExample的示例代码并点击UI中的Crash按钮(demo示例)发生崩溃(连续点击几次):123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. //block内存变化示例 //[self blockMemoryChangeExample]; //block关键字变化引用示例 [self blockExample]; //block循环引用示例 //[self cycleReferenceBlockExample];}- (IBAction)clickCrashButton:(id)sender{ self.isUseCrashCode = YES; [self blockExample];}- (void)blockExample { int a = 49; // __weak //使用__weak也能阻止编译器对block进行copy操作,而且更加安全,这里使用__unsafe_unretained只是试验关键字的效果 __unsafe_unretained //此处加上__unsafe_unretained是因为在ARC环境下可以阻止Clang编译器对block进行copy操作,从而保持block的内存类型是栈类型 void (^ddkitBlockNoCopy)() = ^(){ int b = a; /* 因为引用了block之外的外部变量,所以是栈block 这里运行到了打印ddkitBlockTwo的时侯崩溃,因为使用了__unsafe_unretained关键让ARC环境下不对block进行copy操作。 结合打印的日志,这里证明了2点: 1.__unsafe_unretained确实在ARC的环境下让block赋值操作时不自动copy,从而保证了block的内存类型是栈block(__NSStackBlock__); 2.__unsafe_unretained确实不安全,在运行到}结束时,指针对象已经被清理,但指针还是保存着之前的地址,再访问时造成了EXC_BAD_ACCESS */ #warning 因为ddkitBlockTwo是局部变量,不存在循环引用的问题,可以放心地在{}引用self if (self.isUseCrashCode) { NSLog(@\"this block type is %@ b is %d\", ddkitBlockNoCopy, b); } else { NSLog(@\"this is stack block, b is %d\", b); } }; NSLog(@\"this block type is %@\", ddkitBlockNoCopy); //这里能正确打印block的类型,此处为__NSStackBlock__ ddkitBlockNoCopy(); //在ARC环境下,默认关键字是__strong,会对block进行copy操作,所以block的内存类型会变成堆类型;同理在MRC环境下需要显示调用copy方法来把GlobalBlock或者StackBlock转换成MallocBlock void (^ddkitBlockMalloc)() = ^(){ int b = a + a; //引用了外部变量,并且把这个block赋值给了ddkitBlockTwo(有copy操作),所以是__NSMallocBlock__ NSLog(@\"this block type is %@ b is %d\", ddkitBlockMalloc, b); //因为ddkitBlockThree是局部变量,在ARC环境下到了{}之外就会被清理,所以会打印null空指针 }; NSLog(@\"this block type is %@\", ddkitBlockMalloc); ddkitBlockMalloc();} 代码执行的结果如下: 总结分析:代码运行到了打印ddkitBlockNoCopy的时侯发生崩溃–EXC_BAD_ACCESS:因为ddkitBlockNoCopy使用了__unsafe_unretained关键字,所以在ARC环境下没有对ddkitBlockNoCopy进行copy操作,从而ddkitBlockNoCopy保持了栈内存Block。当程序运行到『}』结束时,ddkitBlockNoCopy指针内存已经被清理,但指针还是保存着地址(指针没有置nil),接着在Block内部访问ddkitBlockNoCopy指针时造成了EXC_BAD_ACCESS。结论:__unsafe_unretained确实不安全,没有及时把指针置成nil是非常有风险的事情。我们需要__unsafe_unretained替换成__weak,这样程序才是安全的。 Block的循环引用1234567891011121314151617181920212223242526272829303132333435//interface typedef void(^ CycleReferenceBlock)(void);@property (nonatomic, copy) CycleReferenceBlock block; //声明block成员变量,ARC环境下,用strong与copy关键字都一样,都会对block内存进行copy- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. //block内存变化示例 //[self blockMemoryChangeExample]; //block关键字变化引用示例 //[self blockExample]; //block循环引用示例 [self cycleReferenceBlockExample];}//implementation- (IBAction)cycleReferenceBlockExample{ if (self.switchCycleReferen.on) { __weak __typeof(self) bSelf = self; //因为block是self的成员变量,会造成循环引用的问题,所以在block外部先__weak一次打破循环引用 self.block = ^() { NSLog(@\"this is cycle reference in blocks, result is %d, cycle reference is broken, this viewContoller can dealloc\", bSelf.isUseCrashCode); }; self.block(); } else { self.block = ^() { NSLog(@\"this is cycle reference in blocks, result is %d, cycle reference is happened, this viewContoller can't dealloc\", self.isUseCrashCode); }; self.block(); }} 代码运行的结果如下:结论:只有堆内存的block才有可能发生循环引用,本栗子中cycleReferenceBlock被self进行copy持有,然后如果在block内部引用self的话就存在了block持有self、self持有block的典型Block循环引用问题,所以当前的UIViewController无法释放。在block外部先把self进行__weak方法把self弱引用成weakSelf,在block内部使用弱引用的weakSelf时就能正常释放UIViewController了。 Objective-C与C++混编(CF对象)本人有过OC与C++对象混编的经验,所以把bridge的相关内存管理问题简单描述下:首先,ARC环境只适用Objective-C/Swift对象,C++对象(包含苹果的CF对象)都需要开发者手动内存管理。先来讲解下ARC环境下OC与C++混编时用到的关键字:__bridge:CF对象(C++对象)桥接OC对象,没有牵扯到对象所有权(主要是内存管理的权限)交接;这就意味着OC创建的对象要在OC端释放,C++创建的对象要在C++端释放,两者对象互不影响。__bridge_transfer:常用在将CF对象(C++对象)转换成OC对象时,将CF对象(C++对象)的所有权交给OC对象,此时ARC就能自动管理该内存;(作用同CFBridgingRelease())使用这个关键字以后C++创建的对象不需要在C++端释放,ARC会对其进行内存管理,如果C++端把内存释放,OC端会出现EXC_BAD_ACCESS。__bridge_retained:(与__bridge_transfer相反)常用在将OC对象转换成CF对象时,将OC对象的所有权交给CF对象来管理;(作用同CFBridgingRetain())使用这个关键字以后OC的对象会被C++来管理,如果OC对象提前释放了,会造成C++端EXC_BAD_ACCESS。 深拷贝与浅拷贝深拷贝是内容拷贝,产生了新的指针,内存生命周期重新开始;浅拷贝是指针拷贝,retainCount+1。 系统对象的可变与不可变对象immutable不可变对象(NSString、NSDictionary、NSArrary、NSSet等)mutable可变对象 (NSMutableString, NSMutableDictionary, NSMutableArray, NSMutableSet等) 集合与非集合对象在非集合类对象中:对immutable对象进行copy操作,是指针复制,mutableCopy操作时内容复制;对mutable对象进行copy和mutableCopy都是内容复制。用代码简单表示如下:1234[immutableObject copy] // 浅复制[immutableObject mutableCopy] //深复制[mutableObject copy] //深复制[mutableObject mutableCopy] //深复制 在集合类对象中,对immutable对象进行copy,是指针复制,mutableCopy是内容复制;对mutable对象进行copy和mutableCopy都是内容复制。但是:集合对象的内容复制仅限于对象本身,对象元素仍然是指针复制。用代码简单表示如下:1234[immutableObject copy] // 浅复制[immutableObject mutableCopy] //单层深复制[mutableObject copy] //单层深复制[mutableObject mutableCopy] //单层深复制 代码示例:123456789101112131415161718192021222324- (void)copyExample{ _dict = [[NSDictionary alloc] initWithObjectsAndKeys:@\"1\",@\"key\",nil]; NSLog(@\"dict retainCount is %lu\", _dict.retainCount); NSDictionary *dict2 = [self.dict copy]; NSLog(@\"dict2 retainCount is %lu, dict retainCount is %lu\", dict2.retainCount, self.dict.retainCount); NSDictionary *mutableCopyDict = [self.dict mutableCopy]; NSLog(@\"mutableCopyDict retainCount is %lu, dict retainCount is %lu\", mutableCopyDict.retainCount, self.dict.retainCount); NSLog(@\"dict addess is %p dict2 address is %p mutableCopyDict address is %p\", self.dict, dict2, mutableCopyDict); [_dict release]; [_dict release]; //因为immutable类型对象copy是指针拷贝,没有产生新的指针,只是在原来的指针retainCount+1,所以可以对_dict进行2次release而不发生崩溃,第二次[_dict release]与[dict2 release]等价 [mutableCopyDict release]; //内容复制产生了新的指针,需要手动释放才能避免内存泄漏。 _mDict = [[NSMutableDictionary alloc] initWithObjectsAndKeys:@\"1\",@\"key\",nil]; NSLog(@\"mDict retainCount is %lu\", _mDict.retainCount); NSDictionary *dict3 = [self.mDict copy]; NSLog(@\"dict3 retainCount is %lu, mDict retainCount is %lu\", dict3.retainCount, self.mDict.retainCount); NSDictionary *dict4 = [self.mDict mutableCopy]; NSLog(@\"dict4 retainCount is %lu, mDict retainCount is %lu\", dict4.retainCount, self.mDict.retainCount); NSLog(@\"mDict addess is %p dict3 address is %p, dict4 address is %p\", self.mDict, dict3, dict4); [_mDict release]; [dict4 release]; //内容复制产生了新的指针,需要手动释放才能避免内存泄漏。 [_mDict release]; //因为mutable类型对象copy以后产生了新的指针,对mDict进行释放是发生崩溃} 运行结果:广告:更多代码demo可以进入我的github进行下载查看运行结果,基本上每行关键代码都有详细的注释。本文中出现的代码都在这个项目下面,请结合项目代码阅读本文,效果更佳。 结合实例代码和运行结果总结:immutable对象copy以后引用计数+1,mutableCopy产生新的指针地址,内存管理重新开始;mutable对象copy与mutableCopy都产生了新的指针地址,内存管理重新开始。 为什么我们@property一个NSString使用copy修饰?因为NSString是一个immutable对象,使用copy是指针复制(与retain效果一样),同时NSString可以指向NSMutableString创建的指针,使用copy会对NSMutableString的指针进行内容拷贝(产生新的指针),这时如果NSMutableString的指针内容即使发生了改变,也不会影响到NSString的指针内容。这个原理也适用NSDictionary、NSSet和NSArray等系统对象。我们可以根据自己的需求来选择strong或者copy关键字来修饰相应的属性。 关于自定义对象的copy上文中提到的对象都是系统级的对象,自定义对象要实现copy方法就要实现NSCopying协议。如果对没有实现NSCopying协议(主要是copyWithZone:方法的实现)的对象进行copy操作会发生崩溃,报*** Terminating app due to uncaught exception 'NSInvalidArgumentException', 'reason: '-[XXX copyWithZone:]: unrecognized selector sent to instance 0xXXXXXX'。 参考资料:Working with BlocksToll-Free Bridged Types深拷贝与浅拷贝","tags":[{"name":"Objective-C","slug":"Objective-C","permalink":"http://www.dejohndong.com/tags/Objective-C/"},{"name":"iOS基础","slug":"iOS基础","permalink":"http://www.dejohndong.com/tags/iOS基础/"},{"name":"内存管理","slug":"内存管理","permalink":"http://www.dejohndong.com/tags/内存管理/"},{"name":"Block","slug":"Block","permalink":"http://www.dejohndong.com/tags/Block/"},{"name":"C++混编","slug":"C-混编","permalink":"http://www.dejohndong.com/tags/C-混编/"}]},{"title":"『钻』研iOS 之 内存管理(一)","date":"2017-02-05T01:20:33.000Z","path":"2017/02/05/「钻」研iOS 之内存管理1/","text":"iOS内存管理在我这6年工作经验过程中的变化可谓是翻天覆地,由于ARC(Automatic Refenerce Counting)的出现,大大简化了iOS开发者的内存管理优化工作。ARC不是垃圾回收机制,虽然开发者不用像以前那样刻意关心内存管理问题,但也不是意味着我们不需要了解iOS的内存管理。 堆(Heap)与栈(Stack)讲到内存,不得不先讲下堆栈,堆和栈的定义大家自己百度百科,本文不再展开,我只阐述自己的总结:栈(操作系统):由操作系统(编译器)自动分配释放,存放函数的参数值,局部变量的值、常量(int、bool)等。其操作方式类似于数据结构中的栈(Stack)。堆(操作系统):一般由程序员分配释放,一般的指针对象创建都存放在堆区,若程序员不释放,程序结束时可能由OS回收,分配方式类似于链表(Queue)。本文主要围绕着堆区(Heap)的内存管理进行讲解。 Reference Counting先讲「Reference Counting」引用计数,不管是MRC或者ARC都会有这个Reference Counting。Objective-C的内存管理方式采用的是保留计数(retainCount)的方式来保证内存的可用性,内存初始化(alloc&init)的时候retainCount为1,当内存被其他指针引用(retain)一次以后该内存的retainCount会+1,当引用的这块内存的这个指针被释放(release)一次以后该内存的retainCount会-1,当retainCount=0时内存会被标记为可回收,会执行dealloc方法进行真正的资源释放销毁操作。如果内存在不再使用的时候retainCount没有变成0(指针已经置成nil,但当时分配的对象没有release的情况下,形成了野指针),那就是内存泄露(Memory leak);如果内存在使用的时候retainCount已经为0(指针的地址还是存在,但这个地址已经被release的情况下)的时候,那就是内存溢出(EXC_BAD_ACCESS)。引用计数的概念在很多语言中都有体现:比如C++的智能指针:std::shared_ptr。还有更多Reference Counting的说明请看Wiki。 MRC时代(Before 2012)广告:更多代码demo可以进入我的github进行下载查看运行结果,基本上每行关键代码都有详细的注释。本文中出现的代码都在这个项目下面,请结合项目代码阅读本文,效果更佳。 MRC(Mannual Reference Counting)手动引用计数内存管理,就是Objective-C对象的内存的创建和释放需要开发者手动管理。先看一段非在NSAutoReleasePool的代码栗子(UI元素相关的「简单」代码):12345678910111213141516171819202122232425262728293031323334353637383940414243//interface@property (nonatomic, retain) UIButton *btnMRC; //MRC用retain修饰对象,保持内存持有//implementation- (void)dealloc{ NSLog(@\"_btnMRC.retainCount is %lu\", _btnMRC.retainCount); //[_btnMRC release]; //这里release需要注意,如果构建的指针对象是通过实例方法创建如alloc、new等,这里需要release一次,否则 NSLog(@\"_btnMRC.retainCount is %lu\", _btnMRC.retainCount); [super dealloc];}- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. NSLog(@\"_btnMRC.retainCount is %lu\", _btnMRC.retainCount); _btnMRC = [UIButton buttonWithType:UIButtonTypeCustom]; //类方法创建的对象在ARC的情况下会默认加上autorelease,所以在dealloc不需要再手动释放 //_btnMRC = [[UIButton alloc] init]; //实例方法创建的对象,需要dealloc的时候进行一次release释放 NSLog(@\"_btnMRC.retainCount is %lu\", _btnMRC.retainCount); [self.view addSubview:_btnMRC]; //addSubView方法会retainCount+1 NSLog(@\"_btnMRC.retainCount is %lu\", _btnMRC.retainCount);}- (void)viewDidAppear:(BOOL)animated{ [super viewDidAppear:animated]; [_btnMRC setFrame:CGRectMake(100, 100, 100.0, 20)]; [_btnMRC setTitle:@\"MRC Button\" forState:UIControlStateNormal]; _btnMRC.backgroundColor = [UIColor redColor];}- (void)viewDidUnload{#warning iOS 6.0以前如果收到memory warning的警告,系统会先viewDidUnload来处理view的释放,如果不在viewDidUnload里处理释放操作,然后系统会重新加载viewDidLoad方法,如果那里的UI内存没有处理得很好,很容易造成野指针的内存泄露。iOS 6.0以后已经废弃了这个方法,跟进ARC的进度// [self.btnMRC release]; [super viewDidUnload];}- (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated.#warning iOS 6.0 以后在这里处理非UI资源的释放内存} PS:这里我使用的是内部变量的指针(带_),从Xcode 4.4版本开始已经可以不显示调用@synthesize来合成get/set方法了,在.m文件内部可以直接用_xxx替代@property声明了变量xxx。这个也是Clang编译器的功劳。 这么多代码只完成了一个简单Button的手动引用计数内存管理,那么在一个复杂界面的UIViewController中有很多的UI元素和其他成员变量,每个对象都需要这么细心地去处理内存管理,这是一件多么恐怖的事情。 MRC时代的UIViewController实现文件随随便便都是好几千行代码,不管是阅读还是维护,都是让人心里抓狂的。 再来看个NSAutoReleasePool的代码栗子:123456789101112131415161718192021- (IBAction)testAutoReleasePool{ NSLog(@\"====== autoreleasepool method begin ======\"); //增加打印分割标识 NSAutoreleasePool *pool = nil; //声明一个显式的NSAutoReleasePool if (self.switchShowAutoReleasePool.on) {//是否初始化显式的NSAutoReleasePool pool = [[NSAutoreleasePool alloc] init]; //显式创建一个NSAutoReleasePool } for (int i = 0; i < 5; i++) { if (self.switchAutoRelease.on) { __unused DDObject *object = [[[DDObject alloc] init] autorelease]; //有autorelease关键字方法,会自动加标识到NSAutoReleasePool里去:如果有显式创建的pool存在,该对象会在显式的Pool里被标识,当Pool释放时该对象;如果没有显示的Pool,会加到系统自动创建的NSAutoReleasePool(隐式的Pool)里去,跟NSRunLoop有关,该Pool会在当次RunLoop结束时执行release。通常是『{}』的}的时候触发,看打印结果可知。 } else { DDObject *object = [[DDObject alloc] init]; [object release]; //创建以后就release释放 } } if (self.switchShowAutoReleasePool.on) {//是否初始化显式的NSAutoReleasePool [pool release]; } NSLog(@\"====== autoreleasepool method end ======\"); //增加打印分割标识} 以上代码的运行结果如下: 广告:更多代码demo可以进入我的github进行下载查看运行结果,基本上每行代码都有详细的注释。本文中出现的代码都在这个项目下面,请结合项目代码阅读本文,效果更佳。 NSAutoReleasePool的显式调用会改变对象的生命周期,原本应该在方法块外部执行dealloc的对象会被提前执行,所以NSAutoReleasePool以及autorelease关键字方法的不合理使用,都会造成一些莫名奇妙的问题,如果你不了解iOS内存管理,在那时候真的很难写出漂亮的代码出来。 下面来解释下出现的关键字方法:alloc&init:通常情况下2个方法是同时出现的,初始化一个对象,分配内存空间,所以当前对象的retainCount为1;retain:对象的retainCount+1;release:对象的retainCount-1;copy:对象的retainCount+1;addSubview:仅限于UI的元素对象。同retain的效果,retainCount会+1;同理removeFromSuperView方法调用的时候会retainCount-1;removeFromSuperView方法通常由系统自动完成;autorelease:会把对象加入到就近的NSAutoReleasePool(显式优先),当Pool释放时对该对象进行一次release操作。dealloc:当对象的retainCount为0的时候会调用这个方法,用来销毁对象的内部处理。 dealloc的线程小知识dealloc方法的执行线程是对象最后一次release的线程,这里就会存在一个问题:如果在dealloc里发生了非常耗时的操作,就会出现主线程卡住的情况,通常我们会重载UIViewController的dealloc方法(基本上都是主线程)来释放一些资源,比如通知(NSNotification)的移除、C++的跨平台库的析构等。如果C++的跨平台库的析构出现了耗时操作,很有可能会卡我们的主线程,所以使用的时候要格外的注意。 PS:写这么多代码我只是想表达之前手动管理有多么地复杂,iOS开发的入门门槛也比现在高很多。最重要的事,我只是想表达:我确确实实是从那个时代过来了,真真实实得写了这么多年的代码>_< 好在苹果的工程师们早早得注意到了这个问题,设计了这个Clang编译器以及ARC的内存管理方式,替开发者来处理内存管理的事情,大大促进了iOS的开发效率,也大大降低了iOS开发的门槛。 ARC时代(2012~至今)ARC(Automatic Reference Counting)自动引用计数内存管理,通过编译器(Clang Complier),本质上还是会使用到retain、release等关键字方法,只是不是开发者手动添加,而是编译器在编译过程中添加retain、release等关键字方法到相应的代码行。 还是先来看看之前的代码在ARC环境下是怎么样的:123456789101112131415161718192021222324252627282930//interface@property (nonatomic, strong) UIButton *btnARC; //ARC环境下用strong保持强引用关系//implementation- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. /* ARC环境下,Clang编译器会对_btnARC进行retain方法,所以开发者无需显示调用retain方法,而且Clang编译器已经在ARC环境下把retain方法标记为不可用。 */ _btnARC = [UIButton buttonWithType:UIButtonTypeCustom]; //ARC情况下不需要特别关心内存管理,开发门槛大大降低。 //_btnARC = [[UIButton alloc] init]; [self.view addSubview:_btnARC]; . . .}- (void)viewDidAppear:(BOOL)animated{ [super viewDidAppear:animated]; [_btnARC setFrame:CGRectMake(100, 100, 100.0, 20)]; [_btnARC setTitle:@\"ARC Button\" forState:UIControlStateNormal]; _btnARC.backgroundColor = [UIColor redColor];}- (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated.} 啥?只有么点?对,确实只要这些,编译器已经帮开发者加了上内存管理的方法(retain/release)代码,所以ARC下无需特别关注iOS的内存管理。只需要了解以下关键字即可:__strong/strong:强引用,与MRC下的retain对应,在编译阶段加上的是retain方法,会对引用计数作增加;__weak/weak:弱引用,引用计数不发生变化,某些特定情况下与MRC的assign对应;copy:一般情况下是浅拷贝,某些特定条件下是深拷贝(下一讲我会仔细讲解『深拷贝与浅拷贝』);__autoreleasing:ARC环境下标识autorelease方法关键字,在编译阶段加上的是autorelease方法;__unsafe_unretained:弱引用,引用计数不发生变化,与weak的区别是这个处理在内存对象被释放以后不会对指针地址置成nil,weak会在释放对象以后把指针地址置成nil。 再来看下AutoReleasePool在ARC的情况下的代码实现:123456789101112131415161718192021222324- (IBAction)testAutoReleasePool{ NSLog(@\"====== autoreleasepool method begin ======\"); //增加打印分割标识 if (self.switchShowAutoReleasePool.on) {//是否初始化显式的@autoreleasepool @autoreleasepool { //创建显示autorelease pool,与NSAutoReleasePool的效果一样 [self doForMethod]; //执行for循环方法 } } else { [self doForMethod]; } NSLog(@\"====== autoreleasepool method end ======\"); //增加打印分割标识}- (void)doForMethod { for (int i = 0; i < 5; i++) { if (self.switchAutoRelease.on) { //是否使用__autoreleasing关键字 __unused __autoreleasing DDObject *object = [[DDObject alloc] init]; //有__autoreleasing关键字方法,与MRC下的autorelease效果一样 } else { __unused DDObject *object = [[DDObject alloc] init]; //编译器会自动加上release } }} 广告:更多代码demo可以进入我的github进行下载查看运行结果,基本上每行关键代码都有详细的注释。本文中出现的代码都在这个项目下面,请结合项目代码阅读本文,效果更佳。 执行这段代码的打印效果与MRC是一模一样的(同样参考MRC运行的结果图)。只是@autoreleasepool{}调用更加简单,从代码和运行结果中也可以得出结论:默认状态下编译器(Clang Complier)对Objective-C的对象只会加上release的关键字方法(打印信息是alloc与dealloc交叉的那种情况),相对来说编译器并没有想象中的那么智能。在高级编程情况下还是需要开发者合理地使用@autoreleasepool和__autoreleasing关键字来控制对象的生命周期。如何合理?就是需要明白iOS的内存管理的精髓–真正了解MRC的工作原理。 从AutoReleasePool的栗子可以分析出来,ARC与MRC内存管理的底层实现其实没有什么变化,只是苹果的工程师们在ARC环境的设计理念下花了大量精力把内存管理的工作交给了编译器来处理,简化开发内存管理的工作。 assign与weak的区别assign:ARC&MRC环境下通用,通常修饰的是常量,比如int、bool等;在MRC环境下,@property时可以修饰(id对象)来防止循环引用、可以修饰IBOutlet出来的UI元素对象、当然也可以修饰不想对retainCount作增加的对象(引用计数不发生变化);weak:只能在ARC环境下使用,weak只能修饰OC对象(包含delegate),不能修饰常量或者其他非OC对象。 下一讲我会讲解『block内存管理』、『C++混编的内存管理』和『深拷贝与浅拷贝』。 参考资料:堆栈Automatic Reference CountingTransitioning to ARC Release NotesClang","tags":[{"name":"Objective-C","slug":"Objective-C","permalink":"http://www.dejohndong.com/tags/Objective-C/"},{"name":"iOS基础","slug":"iOS基础","permalink":"http://www.dejohndong.com/tags/iOS基础/"},{"name":"内存管理","slug":"内存管理","permalink":"http://www.dejohndong.com/tags/内存管理/"},{"name":"ARC & MRC","slug":"ARC-MRC","permalink":"http://www.dejohndong.com/tags/ARC-MRC/"}]},{"title":"[源码]iOS快速建模之DDModel","date":"2015-05-04T23:06:30.000Z","path":"2015/05/05/iOS快速建模之DDModel/","text":"DDModel概要不知不觉我已经毕业快4年了,iOS开发也做了4年多的时间了,也有了一定的经验积累,这次我将着重介绍我封装的模型基类–DDModel。DDModel封装了SQLite、HTTP以及JSON/XML的ORM特性,能快速搭建一个具有本地持久化,快速获取HTTP请求数据以及NSDictionary ORM到模型的模型层工具类,你只要根据自己的业务建模,极大地提高了开发的效率,把更多的精力放在UI的编写中去。 DDModel介绍 DDModel继承了SQLitePersistentObject,这样就快速集成了SQLite存储ORM到对象的过程,之前的一篇文章有介绍; DDModel封装了基于AFNetworking的HTTP请求,简化了大部分开发者把http请求放在Controller层的操作,起到了解耦合的作用; DDModel封装JSON/XML到Model的功能,使JSON/XML ORM到对象模型。使用到的第三方库分别是JTObjectMapping和XMLDictionary; DDModel也封装了HUD的功能,使用了MBProgressHUD,这样你再也不必为HUD烦恼了; DDModel支持基于SQLite的Cache功能; DDModel支持RESTfulAPI。 如何获取代码? 方法1:通过CocoaPods安装DDModel:pod search ‘DDModel’, 然后在你的Podfile中添加最新的版本pod ‘DDModel’, ‘~> 0.4’,这是最快捷的方法,也是我强烈推荐的方法 方法2:通过github的代码仓库获取DDModel;你可以把该项目中的DDModel/Classes目录下的所有文件拷贝到你的项目里,然后再把DDModel依赖的第三方库:AFNetworking、XMLDictionary、JTObjectMapping、SQLitePersistentObject、MBProgressHUD都要拷贝到你的项目。 如何使用DDModel?DDModelHTTPClient参考Demo项目,你可以在你的AppDelegate里加入以下代码来启动一个DDModelHttpClient:1234567- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. [DDModelHttpClient startWithURL:@“https://api.app.net/“ delegate:self]; // initialzie a DDModelHttpClient return YES;} 这样你就启动了一个DDModelHTTPClient了,你可以通过DDModelHttpClientDelegate123456789101112131415161718192021222324252627282930313233@protocol DDHttpClientDelegate <NSObject>@optional/** * Parameter encode method if you should encode the parameter in your HTTP client * * @param params original parameters * * @return endcoded parameters */- (NSDictionary *)encodeParameters:(NSDictionary *)params;/** * Response String decode methods in you HTTP client * * @param responseString origin responseString * * @return new responseString */- (NSString *)decodeResponseString:(NSString *)responseString;/** * Check the response values is an avaliable value. e.g. You will sign in an account but you press a wrong username/password, server will response a error for you, you can catch them use this protocol methods and handle this error exception. * * @param values should check value * @param failure failure block * * @return true or false */- (BOOL)checkResponseValueAvaliable:(NSDictionary *)values failure:(DDResponseFailureBlock)failure;@end 以上这些方法来定制自己的业务逻辑。当然你可以在DDModelHTTPClient里使用AFNetworking里的所有功能。 DDModel然后你就可以根据自己的业务创建各种继承于DDModel的模型了。 举例: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677@interface User : DDModel@property (nonatomic, strong) NSNumber *id;@property (nonatomic, copy) NSString *username;@property (nonatomic, copy) NSString *avatarImageURLString;@end@interface Post : DDModel@property (nonatomic, copy) NSString *text;@property (nonatomic, strong) NSNumber *id;@property (nonatomic, strong) User *user;+ (void)getPostList:(id)params parentVC:(id)viewController showHUD:(BOOL)show success:(DDResponseSuccessBlock)success failure:(DDResponseFailureBlock)failure;@end#import “Post.h”@implementation Post/* * 对象解析的节点实现 */+ (NSString *)parseNode{ return @“data”;}/* jsonMapping 实现 * 使用场景: * 1.返回数据与数据模型不一致时通过映射对应赋值,举例: @{@“id”:@“userId”} 返回数据中的id value 会赋值给数据模型的userId * 2.对象的嵌套关系,如本例里Post对象嵌套了User对象 */+ (NSDictionary *)parseMappings{ /** * in [mappingWithKey:mappings:] method, key is your defined property key; * 在’mappingWithKey:mapping:’中的key值是您定义的属性名名称,所以JSON字符串中的Value会映射给该属性字段; */ id userHandler = [User mappingWithKey:@“user” mapping:[User parseMappings]]; /** * this ‘user’ key is the JSON String’s Key * 这个字典中的’user’ key值就是 JSON 字符串中的 user key; */ NSDictionary *jsonMappings = @{@“user”:userHandler}; /** * 所以整个JSON的映射关系就是把JSON字符串的User内容映射给我定义属性的user属性里,内部递归的关系按照user的parseMapping执行 */ return jsonMappings;}+ (void)getPostList:(id)params parentVC:(id)viewController showHUD:(BOOL)show success:(DDResponseSuccessBlock)success failure:(DDResponseFailureBlock)failure{ [[self class] get:@“stream/0/posts/stream/global” params:params showHUD:show parentViewController:viewController success:success failure:failure];}@end@implementation User+ (NSDictionary *)parseMappings{ //支持keyPath的方式进行映射对象,可以随意重构数据对象 return @{@“avatar_image.url”:@“avatarImageURLString”};}@end 你可以将更多的方法封装在该派生的模型里。 DDModel同时也支持从数据缓存中获取结果:123456789101112131415161718192021222324252627282930313233343536373839/** * Get json data first from db cache then from http server by HTTP GET Mehod. * * @param path HTTP Path * @param params GET Paramtters * @param show is show the HUD on the view * @param viewController parentViewController * @param dbResult db cache result block * @param success success block * @param failure failre block */+ (void)get:(NSString *)path params:(id)params showHUD:(BOOL)showparentViewController:(id)viewController dbSuccess:(DDSQLiteBlock)dbResult success:(DDResponseSuccessBlock)success failure:(DDResponseFailureBlock)failure;/** * Get json data first from db cache then from http server by HTTP POST Mehod. * * @param path HTTP Path * @param params GET Paramtters * @param show is show the HUD on the view * @param viewController parentViewController * @param dbResult db cache result block * @param success success block * @param failure failre block * */+ (void)post:(NSString *)path params:(id)params showHUD:(BOOL)showparentViewController:(id)viewController dbSuccess:(DDSQLiteBlock)dbResult success:(DDResponseSuccessBlock)success failure:(DDResponseFailureBlock)failure; 总结DDModel可以简化你的开发工作,把更多的精力放在UI的编写上。","tags":[{"name":"开源代码","slug":"开源代码","permalink":"http://www.dejohndong.com/tags/开源代码/"},{"name":"iOS开发","slug":"iOS开发","permalink":"http://www.dejohndong.com/tags/iOS开发/"}]},{"title":"[源码]iOS数据持久化SQLitePersistentObject","date":"2015-05-04T13:24:19.000Z","path":"2015/05/04/iOS数据持久化之SQLitePersistentObject/","text":"源码概要iOS开发必然离不开的一个话题就是本地持久化,iOS本地持久化的方式有很多,如SQLite、CoreData等原生API,也有技术大牛对SQLite进行抽象封装以后产生的优秀SQLite插件–FMDB等,今天我向大家着重介绍一款被遗忘的SQLite ORM神器–SQLitePersistentObject,我要在iOS中将ORM进行到底。 SQLitePersistentObject介绍SQLitePersistentObject最初的作者是Jeff LaMarche,它支持Cocoa中大部分的数据类型(如:UIImage、NSString、NSNumber、NSData等等)直接存储进入SQLite,是一款相当强大的SQLite ORM工具库。只要你定义的模型继承于SQLitePersistentObject,你就可以通过它定义的私有方法对数据库进行模型的增删改查。 当然他还有一些不支持的数据类型如void*、struct、union等,这些也是Jeff LaMarche希望我们这些后辈们能改进的。 维护中断但是现在Jeff LaMarche已经不再维护这个工具类了,而且随着时间的推移,这个工具的bug也越来越多。 我从2012年开始使用这个工具类,作为这个工具的收益者的我就接过这个接力棒来做SQLitePersistentObject的维护,我也在SQLitePersistentObject中添加了几个异步的方法,一定程度上提高了工具的稳定性和性能,降低了使用的成本,提高了开发效率。 如何获得代码?最直接的方法就是从我的github中直接cloneDDSQLiteKit这个Repository,这个代码仓库已经包含了Demo。你只要拷贝出项目中的SQLitePersistentObject文件下面的所有文件到你的项目里,就可以使用SQLitePersistent的所有功能了。 当然我也把SQLitePersistent发布到了CocoaPods,直接通过以下代码搜索:pod search ‘SQLitePersistentObject’ 如何使用?首先你创建一个新的Class继承于SQLitePersistentObject,然后可以根据自己的业务定义你需要的属性名:例如:123456789101112131415161718header file:#import “SQLitePersistentObject.h”@interface Device : SQLitePersistentObject@property (nonatomic, copy) NSString *name;@property (nonatomic, copy) NSString *model;@property (nonatomic, strong) NSNumber *price;@endimplementation file:#import “Device.h”@implementation Device@end 这样,这个Device类就包含了SQLitePersistentObject的所有功能了。 核心代码介绍我这边介绍一下我添加的一些异步方法:1234567891011121314151617181920212223242526272829303132333435363738#pragma mark - DeJohn Dong Added Methods/** * Asynchronous add/update an object to db. */- (void)save;/** * Asynchronous delete an object from db. */- (void)asynDeleteObject;/** * Asynchronous delete an object and the cascade objects from db. */- (void)asynDeleteObjectCascade:(BOOL)cascade;/** * Asynchronous Query the object list with criteria from db. * * @param criteria criteria string * @param result result list */+ (void)queryByCriteria:(NSString *)criteria result:(DBQueryResult)result;/** * Asynchronous Query the first object with criteria from db * * @param criteria criteria string * @param result result object */+ (void)queryFirstItemByCriteria:(NSString *)criteria result:(DBQueryResult)result;/** * Asynchronous Query all the objects from db * * @param result result list */+ (void)queryResult:(DBQueryResult)result; 这些方法都是基于线程安全的异步操作数据库,你可以放心的使用。 注意事项但这些方法不能和SQLitePersistentObject原始的同步方法混用。 SQLitePersistentObject所有的原始方法都是线程同步的,需要你自己创建多线程来控制异步加载数据,这也是因为大家觉得SQLitePersistentObject不好的最主要原因了。 经过我的改进以后,我相信还是会有不少开发者会喜欢上这个库的。 强烈建议所以我强烈建议使用SQLitePersistentObject的开发者都使用我扩展的异步方法。 总结SQLitePersistentObject虽然是很古老的库,但他还是有很多的内容值得我们去学习的,本篇文章只是介绍了这个库的功能和使用,以后我会推出新的文章具体讲解其内部实现,敬请期待。","tags":[{"name":"开源代码","slug":"开源代码","permalink":"http://www.dejohndong.com/tags/开源代码/"},{"name":"iOS开发","slug":"iOS开发","permalink":"http://www.dejohndong.com/tags/iOS开发/"}]},{"title":"博客空间搬家","date":"2015-04-30T13:50:21.000Z","path":"2015/04/30/博客空间搬家/","text":"事件起因之前我的博客一直挂在我同事服务器的虚拟空间里,使用的是wordpress的个人博客系统。最近一段时间由于服务器故障,我总是发现自己的博客不能被访问。而且又觉得wordpress有很多的使用有限制,于是乎想弃用wordpress的解决方案,改用一些更加方便的好维护的方案来解决以上问题。 解决过程通过搜索发现很多博客达人使用github+hexo的方式搭建免费又可以配置个人域名的博客空间,于是参照该hexo搭建教程把hexo的个人博客给构建起来了 CNAME问题按照上面的教程搭建完成以后配置好了CNAME文件,输入网址http://www.dejohndong.com能顺利访问,但是我每次hexo deploy以后github上的静态网页都会把CNAME文件给remove掉,造成个人域名不能访问。 解决思路通过自己对git的熟悉程度,首先我在我的博客空间http://openboy2012.github.io的git Repository创建一个prepare的分支,然后在我的博客空间的_config.yml文件配置改成如下配置: # Deployment ## Docs: http://hexo.io/docs/deployment.html deploy: type: git repo: https://github.com/openboy2012/openboy2012.github.io.git branch: prepare 这样我的每次deploy会推送到prepare分支上面,再使用git merge命令把prepare分支的修改文章合并到master分支上面,这样CNAME文件就不会因为deploy命令被删除了。保证了每次发布新的文章以后能被及时地访问。 博客空间管理创建博客空间我也是创建一个新的Repository推送到github上来进行管理,这样我就可以在任何一台电脑上把博客空间clone下进行博客文章的撰写了。 WordPress 文章迁移我之前的文章都是写在wordpress上,导出文章的xml。然后安装hexo-migrator-wordpress插件把导出的xml导入到我的hexo博客空间里,不过格式会缺失,需要微调以后才能正常化。 文章编辑hexo创建的文章都是用markdown语法,对于写程序的我们来说应该也不算是个麻烦的事情,推荐几款好一点的markwon编辑器吧。MWeb Lite免费好用的markdown编辑器,本人正在使用中。。。有钱的可以使用MWeb Pro。还有很多其他的编辑器,自己去发掘吧。 总结作为一名程序员,一定要时刻保持着一颗学习的心,技术是在不断的更新中,我们也要不断得学习新的内容来提高自己的能力。加油吧,快乐的程序员,加油吧,快乐的钻钻她爹。","tags":[{"name":"DeJohn Style","slug":"DeJohn-Style","permalink":"http://www.dejohndong.com/tags/DeJohn-Style/"}]},{"title":"iOS学习笔记:NSRunLoop的理解","date":"2015-03-17T07:00:49.000Z","path":"2015/03/17/iOS学习笔记:NSRunLoop的理解/","text":"背景有没有好奇过iOS应用不是像C程序那样执行完main函数以后就退出了,还可以在点击按钮以后弹出一些交互UI,还能通过手势滑动UI(如UIScrollView、UITableView等),这一切都是NSRunLoop在帮忙。 NSRunLoop官方定义官方文档的定义是NSRunLoop,其字面意思是“运行回路”,它是一个循环、可以处理事件,是用来管理线程中输入的资源。iOS应用能在启动以后不退出,就是因为它的存在。主线程中的NSRunLoop是默认开启的但是处于一种“等待”的状态,当有信息输入时NSRunLoop才会发生响应(消息分发,支持异步),所以主线程不会被卡线程。 NSRunLoop的落脚点每个创建的多线程都会自己创建一个NSRunLoop,但是默认非主线程的RunLoop是没有运行的,需要为RunLoop添加至少一个事件源,然后手动去run它。 NSRunLoop处理的事件NSRunLoop能处理的事件有2种:一种是输入源,一种是定时源。输入源包括3种:performSelector源,基于端口(Mach port)的源,以及自定义的源;他们都是用来处理异步事件的;定时源即NSTimer,一般情况下是用来处理同步事件的。 NSRunLoop的使用场景下来是Run Loop的使用场景: 使用port或是自定义的input source来和其他线程进行通信; 在线程(非主线程)中使用NSTimer 使用 performSelector…系列 12- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes; - (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay; 使用线程执行周期性工作 引用别人的一篇文章觉得不错:- (BOOL)runMode:(NSString )mode beforeDate:(NSDate )limitDate方法详解","tags":[{"name":"iOS学习笔记","slug":"iOS学习笔记","permalink":"http://www.dejohndong.com/tags/iOS学习笔记/"},{"name":"iOS消息循环","slug":"iOS消息循环","permalink":"http://www.dejohndong.com/tags/iOS消息循环/"}]},{"title":"iOS学习笔记:Category与Extension的区别","date":"2015-03-11T09:18:49.000Z","path":"2015/03/11/iOS学习笔记:Category与Extension的区别/","text":"前言作为一个有4年以上iOS开发经验的开者者来说,使用Category和Extension的场景应该是数不胜数的,也不知道从Xcode的哪个版本开始,创建一个新的UIViewController类会默认添加一个该类的Extension,而我也习惯于在这个Extension上添加一些不公开的属性。 我的分析仔细查阅了相关资料并且结合自己的实践得出以下结论: Category:字面意思是类别。 主要用来扩展方法,并且适用于subclass; 只能添加readonly的属性, 如果要添加readwrite属性必须在runtime过程中用objc_setAssociatedObject()和objc_getAssociatedObject()方法来实现属性的get与set方法; Extension:字面意思是扩展。 同样可以扩展方法和属性,但局限于原始类; 声明的方法必须在@implemention中实现,不然编译器会报warning;在extension中可以定义可写的属性,公有可读、私有可写的属性(Publicly-Readable, Privately-Writeable Properties)一般这样实现。 总结综上所述,我们通常要封装一些公共方法的时候我们可以考虑使用Category的方式。如果我们想在原始类上面增加一些不公开的方法、属性(私有方法、属性)时可以新建一个Extension来解决问题。","tags":[{"name":"iOS学习笔记","slug":"iOS学习笔记","permalink":"http://www.dejohndong.com/tags/iOS学习笔记/"}]},{"title":"iOS学习笔记:iOS多线程的3种方式之GCD","date":"2015-03-07T02:12:24.000Z","path":"2015/03/07/iOS学习笔记:iOS多线程的3种方式之GCD/","text":"众所周知:iOS的多线程的创建方式有3种:NSThread, NSOperation 和GCD(Grand_Central_Dispatch)。 为什么苹果要出3种多线程呢?答案是创建多线种的需求千变万化,不是所有的方式都能解决需求,所以三者相互共存着。今天我就先来分析GCD的优缺点。 我的见解通过阅读苹果的官方文档和参考了同行中的大牛的理解得出一下结论: 优点: 使用block技术(闭包),使代码看上去十分简洁,使用简单(适合新手); 能自动分配到空闲的处理器内核中,最大限度发挥多核心CPU的性能;缺点: 不能管理线程的生命周期,不能满足某些需求例如图片上传的取消(AFNetworking中就是用NSOperation实现的)。总结:iOS的多线程的这3种方式会一直存在着来满足不同的用户开发需求,三者相辅相成。我们不能一味的说哪种多线程的好与不好,只要找到某个场景下最合适的方法就可以了。 具体的使用我这边就不多讲了,推荐别人写的文章好了: iOS多线程编程之Grand Central Dispatch(GCD)介绍和使用","tags":[{"name":"iOS学习笔记","slug":"iOS学习笔记","permalink":"http://www.dejohndong.com/tags/iOS学习笔记/"},{"name":"iOS多线程","slug":"iOS多线程","permalink":"http://www.dejohndong.com/tags/iOS多线程/"}]},{"title":"iOS学习笔记:iOS多线程的3种方式之NSOperation","date":"2015-03-06T14:42:31.000Z","path":"2015/03/06/iOS学习笔记:iOS多线程的3种方式之NSOperation/","text":"众所周知:iOS的多线程的创建方式有3种:NSThread, NSOperation 和GCD(Grand_Central_Dispatch)。 为什么苹果要出3种多线程呢?答案是创建多线种的需求千变万化,不是所有的方式都能解决需求,所以三者相互共存着。今天我就先来分析NSOperation的优缺点。 我的见解通过阅读苹果的官方文档和参考了同行中的大牛的理解得出一下结论: NSOpeartion必须结合NSOperationQueue来使用,而且NSOperationQueue可以通过 setMaxConcurrentOperationCount:来设置最大并发数量,起到线程池的作用 NSOperation的底层实现是GCD,可能通过断点在main方法实现或者start方法实现时看到堆栈内有GCD的相关内容: 优点: NSOperation不需要关心线程管理,数据同步的事情,可以把精力放在自己需要执行的操作上; NSOperation可以监控多线程的运行状态,随时可以结束任务; NSOperation支持KVO,这样我们通过KVO的方法获知线程中任务的状态; 缺点: 比GCD更高级的抽象,造成了性能上的劣势; 对于新人来说,使用起来比GCD相对复杂; 具体的使用我这边就不多讲了,推荐别人写的文章好了:iOS多线程编程之NSOperation和NSOperationQueue的使用","tags":[{"name":"iOS学习笔记","slug":"iOS学习笔记","permalink":"http://www.dejohndong.com/tags/iOS学习笔记/"},{"name":"iOS多线程","slug":"iOS多线程","permalink":"http://www.dejohndong.com/tags/iOS多线程/"}]},{"title":"iOS学习笔记:iOS多线程的3种方式之NSThread","date":"2015-03-06T14:03:48.000Z","path":"2015/03/06/iOS学习笔记:iOS多线程的3种方式之NSThread/","text":"众所周知:iOS的多线程的创建方式有3种:NSThread, NSOperation 和GCD(Grand_Central_Dispatch)。 为什么苹果要出3种多线程呢?答案是创建多线种的需求千变万化,不是所有的方式都能解决需求,所以三者相互共存着。今天我就先来分析NSThread的优缺点。 我的见解通过阅读苹果的官方文档和参考了同行中的大牛的理解得出以下结论: 优点: NSThread 是轻量级的(大家公认的,Apple最早的多线程技术); 可以管理生命周期,NSThread 是一个对象,必然可以通过相应的方法来管理它的生命周期,通过init方法创建线程实例,start方法让线程真正跑起来,cancel方法让线程取消,每个方法都能让我们合理操作。(ps:其实很多人认为这是它的缺点,使用起来确实复杂了点,但是当你有相应的需求时你会发现这才是你要的最合适的多线程方式,所以看问题是要多角度来分析的,在某些场景下优点与缺点会互换)。 缺点: NSThread需要程序员自己处理其生命周期,数据同步问题(数据锁等),势必会影响了开发效率(ps:我以上的结论是站在不同使用角度的看法); 线程同步以及数据加锁这些操作会影响到线程运行时的性能。 具体的使用我这边就不多讲了,推荐别人写的文章好了:iOS多线程编程之NSThread的使用","tags":[{"name":"iOS学习笔记","slug":"iOS学习笔记","permalink":"http://www.dejohndong.com/tags/iOS学习笔记/"},{"name":"iOS多线程","slug":"iOS多线程","permalink":"http://www.dejohndong.com/tags/iOS多线程/"}]},{"title":"[源码]滚动的数字:FlickerNumber","date":"2015-03-02T14:50:32.000Z","path":"2015/03/02/滚动的数字:FlickerNumber/","text":"起因最近的项目中要求实现支付宝的滚动数字的效果,查找到了一些第三方的代码,但是效果很不理想。 求人不如求己,那我就自己动手来实现该效果。在学习github大牛的代码过程中,看到很多大牛都是用Category来扩展实现某些功能,例如: SDWebImage中的UIImageView的Category、AFNetworking中的UIButton的Category等。 那我也尝试着使用Category的方式来实现该数字滚动的效果。 滚动思路众所周知,UIKit中的UILabel控件格外强大,在开启iOS的动效以后,UILabel上的内容变换都会产生动画效果。那我就可以用UILabel的这个特性来实现数字的滚动效果。 设计思路:让数字从某个起点数字累加同一个平均数直到大于或等于目标数字,每次累加的数字结果设置为这个UILabel的text值,这个过程会形成动画,正好达到一个数字变化的效果。 当然我们可以控制刷新的间隔。 代码实现新建UILabel的Category: UILabel+FlickerNumber 首先,数字滚动的动画是一个过渡过程,我需要一个中间变量。我的第一反应是增加一个静态变量作为中间变量,但是细细想来,如果我有2个UILabel同时滚动数字,那么这个静态变量会贯穿这2个UILabel,这样滚动动画肯定会产生问题的。所以这里是不能使用静态变量来作为中间变量的。 然后我就想到了使用属性,类似UILabel的text属性一样,相当于我对UILabel的属性进行扩展。 使用Category增加属性必须要使用iOS的runtime的特性:12345678910111213141516171819202122#import <objc/runtime.h>@interface UILabel ()@property (nonatomic, strong, readwrite) NSNumber *flickerNumber;@property (nonatomic, strong, readwrite, nullable) NSNumberFormatter *flickerNumberFormatter;@property (nonatomic, strong, readwrite, nullable) NSTimer *currentTimer;@end@implementation UILabel (FlickerNumber)//The intermediate number, it's private variable. Extend property use the runtime feature.- (void)setFlickerNumber:(NSNumber *)flickerNumber { objc_setAssociatedObject(self, @selector(flickerNumber), flickerNumber, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}- (NSNumber *)flickerNumber { return objc_getAssociatedObject(self, _cmd);}... 这样我就完成了对UILabel扩展了一个flickerNumber的属性,因为我把这个属性设置在了extentsion里,所有该属性只能在这个类实现内部使用,别人在外部是看不到这个属性的。 依葫芦画瓢,再增加flickerNumberFormatter、currentTimer这2个属性: 1234567891011121314151617181920...//Flicker animation timer.- (void)setCurrentTimer:(nullable NSTimer *)currentTimer { objc_setAssociatedObject(self, @selector(currentTimer), currentTimer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}- (nullable NSTimer *)currentTimer { return objc_getAssociatedObject(self, _cmd);}- (void)setFlickerNumberFormatter:(nullable NSNumberFormatter *)flickerNumberFormatter { objc_setAssociatedObject(self, @selector(flickerNumberFormatter), flickerNumberFormatter, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}- (nullable NSNumberFormatter *)flickerNumberFormatter { return objc_getAssociatedObject(self, _cmd);}... 计时器currentTimer:用来控制时间间隔和传值,定时去刷新UILabel的数字text;数字格式化输出flickerNumberFormatter:是用来格式化输出数字的。 扩展属性都声明好了,那就开始撰写实现代码了,先来一个核心代码:12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758- (void)fn_setNumber:(NSNumber *)number duration:(NSTimeInterval)duration format:(nullable NSString *)formatStr numberFormatter:(nullable NSNumberFormatter *)formatter attributes:(nullable id)attrs { /** * check the number type */ NSAssert([number isKindOfClass:[NSNumber class]], @\"Number Type is not matched , exit\"); if(![number isKindOfClass:[NSNumber class]]) { self.text = [NSString stringWithFormat:@\"%@\",number]; return; } /* limit duration is postive number and it is large than 0.3 , fixed the issue#1--https://github.com/openboy2012/FlickerNumber/issues/1 */ duration = fabs(duration) < 0.3 ? 0.3 : fabs(duration); [self.currentTimer invalidate]; self.currentTimer = nil; //initialize useinfo dict NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithCapacity:0]; if(formatStr) [userInfo setObject:formatStr forKey:DDFormatKey]; [userInfo setObject:number forKey:DDResultNumberKey]; //initialize variables long long beginNumber = 0; [userInfo setObject:@(beginNumber) forKey:DDBeginNumberKey]; self.flickerNumber = @0; unsigned long long endNumber = [number unsignedLongLongValue]; //get multiple if number is double type int multiple = [self multipleForNumber:number formatString:formatStr]; if (multiple > 0) endNumber = [number doubleValue] * multiple; //check the number if out of bounds the unsigned int length if (endNumber >= INT64_MAX) { self.text = [NSString stringWithFormat:@\"%@\",number]; return; } [userInfo setObject:@(multiple) forKey:DDMultipleKey]; [userInfo setObject:@(endNumber) forKey:DDEndNumberKey]; if ((endNumber * DDFrequency)/duration < 1) { duration = duration * 0.3; } [userInfo setObject:@((endNumber * DDFrequency)/duration) forKey:DDRangeIntegerKey]; if(attrs) [userInfo setObject:attrs forKey:DDAttributeKey]; self.flickerNumberFormatter = nil; if(formatter) self.flickerNumberFormatter = formatter; self.currentTimer = [NSTimer scheduledTimerWithTimeInterval:DDFrequency target:self selector:@selector(flickerAnimation:) userInfo:userInfo repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:self.currentTimer forMode:NSRunLoopCommonModes];} 该方法中通过一个可变字典存储了起始数、目标数、中间平均数以及一些数字的扩展属性(MutableAtrributedString、format-string和number-formatter style),这个可变字典通过currentTimer的userInfo属性在函数中传递。 中间平均数是通过目标数值 * 每秒帧数(默认值是1/30--即每秒闪动30次的标准) / 动画时长duration计算获得。 滚动动画代码实现:1234567891011121314151617181920212223242526272829303132333435363738/** * Flicker number animation implemetation method. * * @param timer The schedule timer, the time interval decide the number flicker counts. */- (void)flickerAnimation:(NSTimer *)timer { /** * check the rangeNumber if more than 1.0, fixed the issue#2--https://github.com/openboy2012/FlickerNumber/issues/2 */ if ([timer.userInfo[DDRangeIntegerKey] floatValue] >= 1.0) { long long rangeInteger = [timer.userInfo[DDRangeIntegerKey] longLongValue]; self.flickerNumber = @([self.flickerNumber longLongValue] + rangeInteger); } else { float rangeInteger = [timer.userInfo[DDRangeIntegerKey] floatValue]; self.flickerNumber = @([self.flickerNumber floatValue] + rangeInteger); } int multiple = [timer.userInfo[DDMultipleKey] intValue]; if(multiple > 0) { [self floatNumberHandler:timer andMultiple:multiple]; }else { NSString *formatStr = timer.userInfo[DDFormatKey]?:(self.flickerNumberFormatter?@\"%@\":@\"%.0f\"); self.text = [self finalString:@([self.flickerNumber longLongValue]) stringFormat:formatStr numberFormatter:self.flickerNumberFormatter]; if(timer.userInfo[DDAttributeKey]){ [self attributedHandler:timer.userInfo[DDAttributeKey]]; } if([self.flickerNumber longLongValue] >= [timer.userInfo[DDEndNumberKey] longLongValue]){ self.text = [self finalString:timer.userInfo[DDResultNumberKey] stringFormat:formatStr numberFormatter:self.flickerNumberFormatter]; if(timer.userInfo[DDAttributeKey]){ [self attributedHandler:timer.userInfo[DDAttributeKey]]; } [timer invalidate]; } }} 代码是通过NSTimer的userInfo传值,处理了整型中间平均数和浮点型中间平均数两种情况,方法的核心是通过不断累加这个平均数来递增结果值,然后通过finalString:stringFormat:numberFormatter:方法输出到UILabel的text或者attributedText:12345678910111213141516171819202122232425262728293031/** * The final-string of each frame of flicker animation. * * @param number The result number. * @param formatStr The string-format String. * @param formatter The number-formatter style. * * @return The final string. */- (NSString *)finalString:(NSNumber *)number stringFormat:(NSString *)formatStr numberFormatter:(NSNumberFormatter *)formatter { NSString *finalString = nil; if (formatter) { NSAssert([formatStr rangeOfString:@\"%@\"].location != NSNotFound, @\"The string format type is not matched. Please check your format type if it's not `%%@`. \"); finalString = [NSString stringWithFormat:formatStr,[self stringFromNumber:number numberFormatter:formatter]]; } else { NSAssert([formatStr rangeOfString:@\"%@\"].location == NSNotFound, @\"The string format type is not matched. Please check your format type if it's `%%@`. \"); //fixed the bug if use the `%d` format string. if ([formatStr rangeOfString:@\"%d\"].location == NSNotFound) { finalString = [NSString stringWithFormat:formatStr,[number doubleValue]]; } else { finalString = [NSString stringWithFormat:formatStr,[number longLongValue]]; } } return finalString;} 注意:!!!这边格式化输出的时候我用2个断言来判断格式化输出的类型,如果格式化类型与输出的text不匹配,程序会Crash。 如果使用了numberFormatter就不能使用%f、%d等数字格式化,只能使用%@格式化输出text。相反,如果你想要用格式化数字,则不能用%@替代。 关于数字格式化以及字体颜色变化和字体大小不同,我是通过NSString的format特性和UILabel的NSMutableAtrributedString特性以及numberForamtter特性对输出的字符串进行输出格式的扩展:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950/** * The attributed(s) text handle methods * * @param attributes The attributed property, it's a attributed dictionary OR array of attributed dictionaries. */- (void)addTextAttributes:(id)attributes { if ([attributes isKindOfClass:[NSDictionary class]]) { NSRange range = [attributes[DDDictRangeKey] rangeValue]; [self addAttribute:attributes[DDDictArrtributeKey] range:range]; } else if([attributes isKindOfClass:[NSArray class]]) { for (NSDictionary *attribute in attributes) { NSRange range = [attribute[DDDictRangeKey] rangeValue]; [self addAttribute:attribute[DDDictArrtributeKey] range:range]; } }}/** * Add attributed property into the number text OR string-format text. * * @param attri The attributed of the text * @param range The range of the attributed property */- (void)addAttribute:(NSDictionary *)attri range:(NSRange)range { NSMutableAttributedString *str = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText]; // handler the out range exception if(range.location + range.length <= str.length){ [str addAttributes:attri range:range]; } self.attributedText = str;}/** * Get the number string from number-formatter style. * * @param number The result number. * @param formattor The number-formatter style. * * @return The number string. */- (NSString *)stringFromNumber:(NSNumber *)number numberFormatter:(NSNumberFormatter *)formattor { if(!formattor) { formattor = [[NSNumberFormatter alloc] init]; formattor.formatterBehavior = NSNumberFormatterBehavior10_4; formattor.numberStyle = NSNumberFormatterDecimalStyle; } return [formattor stringFromNumber:number];} FlickerNumber 使用的默认number-formatter style 是:1234567891011/** * Get the decimal style number as default number-formatter style. * * @return The number-foramtter style. */- (NSNumberFormatter *)defaultFormatter { NSNumberFormatter *formattor = [[NSNumberFormatter alloc] init]; formattor.formatterBehavior = NSNumberFormatterBehavior10_4; formattor.numberStyle = NSNumberFormatterDecimalStyle; return formattor;} 如何处理float类型的数据?思路:把float类型 乘以(*) 小数点的位数的倍数变成整型数字,还是从0开始累加到目标整型数字,只是在输出text的时候再除以相应的倍数,到达滚动float类型的数字的效果。 获取Float类型数值倍数方法,这里只处理了最大小数位为6位的情况:123456789101112131415161718192021222324252627282930/** * Get muliple from number * * @param number past number * * @return mulitple */- (int)multipleForNumber:(NSNumber *)number formatString:(NSString *)formatStr { if([formatStr rangeOfString:@\"%@\"].location == NSNotFound) { if([formatStr rangeOfString:@\"%d\"].location != NSNotFound) { return 0; } formatStr = [self regexNumberFormat:formatStr]; NSString *formatNumberString = [NSString stringWithFormat:formatStr,[number floatValue]]; if([formatNumberString rangeOfString:@\".\"].location != NSNotFound){ NSUInteger length = [[formatNumberString substringFromIndex:[formatNumberString rangeOfString:@\".\"].location +1] length]; float padding = log10f(length < 6 ? length:6); number = @([formatNumberString floatValue] + padding); } } NSString *str = [NSString stringWithFormat:@\"%@\",number]; if([str rangeOfString:@\".\"].location != NSNotFound) { NSUInteger length = [[str substringFromIndex:[str rangeOfString:@\".\"].location +1] length]; // Max Multiple is 6 return length >= 6 ? pow(10, 6): pow(10, (int)length); } return 0;} 浮点型数字滚动动画输出: 123456789101112131415161718192021/** * Float number handle method. * * @param timer timer * @param multiple The number's multiple. */- (void)floatNumberHandler:(NSTimer *)timer andMultiple:(int)multiple { NSString *formatStr = timer.userInfo[DDFormatKey] ?: (self.flickerNumberFormatter ? @\"%@\" : [NSString stringWithFormat:@\"%%.%df\",(int)log10(multiple)]); self.text = [self finalString:@([self.flickerNumber doubleValue]/multiple) stringFormat:formatStr numberFormatter:self.flickerNumberFormatter]; if (timer.userInfo[DDAttributeKey]) { [self attributedHandler:timer.userInfo[DDAttributeKey]]; } if ([self.flickerNumber longLongValue] >= [timer.userInfo[DDEndNumberKey] longLongValue]) { self.text = [self finalString:timer.userInfo[DDResultNumberKey] stringFormat:formatStr numberFormatter:self.flickerNumberFormatter]; if(timer.userInfo[DDAttributeKey]){ [self attributedHandler:timer.userInfo[DDAttributeKey]]; } [timer invalidate]; }} 处理滚动动画被打断1[[NSRunLoop currentRunLoop] addTimer:self.currentTimer forMode:NSRunLoopCommonModes]; 当UILabel处于一个滚动的视图中(如UICollectionView、UIScrollView等),在该视图滚动过程中,UILabel的滚动动画会被打断,使用改代码以后可以防止动画被打断。 更多代码获得:https://github.com/openboy2012/FlickerNumberCocoaPods获得方式:pod search 'FlickerNumber'方法的列表可以参考源码和github上的ReadMe. Swift适配FlickerNumber的Swift版本也已经开发完成,完美兼容了XCode7、Swift 2.0的语法,源码已经提交至github。CocoaPods获得方式:pod search 'FlickerNumber-Swift' 总结在写这个控件的时候,用到了很多技术点:runtime(运行时)、数学算法方法(logf()等算法)、UILabel的本身的一些特性(attributedText)、NSNumberFormatter的格式化输出和NSString的stringWithFormat:方法输出。我收获了不少iOS的技术知识和用法。但这些都只是一个程序员该有的基本能力。 真正的意义是在于我这个开源过程。 这个开源过程能告诉其他程序员如何开源自己写的代码,如何写出一个个简单的方法让调用者能简单应用而不用太关注具体的实现,同时也能让善于研究的开发者能理解代码的含义,这才是最困难的。代码的易读性和可维护性是至关重要的。 我相信随着我的不断提高,我写的代码会越来越好。","tags":[{"name":"开源代码","slug":"开源代码","permalink":"http://www.dejohndong.com/tags/开源代码/"},{"name":"iOS开发","slug":"iOS开发","permalink":"http://www.dejohndong.com/tags/iOS开发/"}]},{"title":"[源码]实现iOS的瀑布流:DDCollectionViewFlowLayout","date":"2015-02-25T14:01:32.000Z","path":"2015/02/25/实现iOS的瀑布流:DDCollectionViewFlowLayout/","text":"起因前段时间一直在做iOS客户端的64位适配,所以把开发项目设置成了最低系统要求为iOS6.0。空闲之余,准备把之前用UIScrollView实现的瀑布流用UICollectionView重新实现一下。于是DDCollectionViewFlowLayout就这样诞生了。 学习步骤在学习UICollectionView的过程中,首先肯定是查阅苹果的官方文档UICollectionView。了解UICollectionView的基本信息以后得知要想实现瀑布流的效果必须使用UICollectionViewLayout,继续参考苹果官方文档了解UICollectionViewFlowLayout必须实现的方法和生命周期。了解过UICollectionViewFlowLayout的Protocol方法以后,就可以着手写自己的代码了。首先 DDCollectionViewFlowLayout 继承了UICollectionViewFlowLayout,只要重载以下方法123- (void)prepareLayout; - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect; - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath; 就可以实现瀑布流的效果。 怎么获得代码?可以直接通过下面的链接在github中获取:https://github.com/openboy2012/DDCollectionViewFlowLayout当然你也可以在CocoaPods中搜索:pod search ‘DDCollectioViewFlowLayout’效果图: 怎么使用?如果你只想简单应用,导入DDCollectionViewFlowLayout以后实现以下代码: 1234DDCollectionViewFlowLayout *layout = [[DDCollectionViewFlowLayout alloc] init]; layout.delegate = self; layout.enableSticky = YES; //set header sticky if you want[self.collectionView setLayout:layout]; 然后要实现UICollectionViewDataSource 方法中的必需方法:123456// The cell that is returned must be retrieved from a call to -dequeueReusableCellWithReuseIdentifier:forIndexPath: - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section;- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath; 我只在DDCollectionViewFlowLayout中新增了一个必需实现的delegate方法:1234- (NSInteger)collectionView:(UICollectionView *)collectionView layout:(DDCollectionViewFlowLayout *)layout numberOfColumnsInSection:(NSInteger)section; 因为DDCollectionViewFlowLayout继承了UICollectionViewFlowLayout,所以你可以选择性地实现UICollectionViewFlowLayoutDelegate中非必需delegate方法,例如:1234567891011121314151617181920212223- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section;- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayoutminimumLineSpacingForSectionAtIndex:(NSInteger)section;- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section;- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayoutreferenceSizeForHeaderInSection:(NSInteger)section;- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayoutreferenceSizeForFooterInSection:(NSInteger)section; 这些方法都适配了DDCollectionViewFlowLayout,让你有亲切的感觉。 更多如有不懂可以参考Demohttps://github.com/openboy2012/DDCollectionViewFlowLayout","tags":[{"name":"开源代码","slug":"开源代码","permalink":"http://www.dejohndong.com/tags/开源代码/"},{"name":"iOS开发","slug":"iOS开发","permalink":"http://www.dejohndong.com/tags/iOS开发/"}]},{"title":"你好,访客","date":"2015-02-13T00:52:36.000Z","path":"2015/02/13/你好,访客/","text":"本人2011年开始从事iOS开发,目前混迹于嘉兴,一个小巧的城市,在这里你可以跟我交流iOS方面的所有技术。 我的github地址:http://github.com/openboy2012.","tags":[{"name":"DeJohn Style","slug":"DeJohn-Style","permalink":"http://www.dejohndong.com/tags/DeJohn-Style/"}]}]