挺好的 过程里面有些可取之处

本文详细解析了iOS开发中的RunLoop机制,从程序启动到视图显示,通过RunLoop的内部逻辑,展示了事件处理和方法调用顺序的关键。重点讨论了RunLoop的运行原理,包括与线程和自动释放池的关系,以及如何通过Port-Based Sources、CFRunLoopTimerRef、CFRunLoopObserverRef等组件高效处理事件。同时,文章还介绍了如何利用RunLoop进行界面刷新、手势识别、GCD任务及网络请求的管理,以及如何在特定场景下保持子线程处理事件。通过实例分析,旨在帮助开发者更好地理解和应用RunLoop,避免常见的编程错误,提升应用性能。

buttonTestPressed

学习iOS开发一般都是从UI开始的,从只知道从IB拖控件,到知道怎么在方法里写代码,然后会显示什么样的视图,产生什么样的事件,等等。其实程序从启动开始,一直都是按照苹果封装好的代码运行着,暴露的一些属性和方法作为接口,是让我们在给定的方法里写代码实现自定义功能,做出各种各样的应用。这些方法的调用顺序最为关键,熟悉了程序运转和方法调用的顺序,才可以更好地操控程序和代码,尽量避免Xcode不报错又实现不了功能的BUG。从Xcode的线程函数调用栈可以看到一些方法调用顺序。


--零--从程序启动开始到view显示:


start->(加载framework,动态静态链接库,启动图片,Info.plist,pch等)->main函数->UIApplicationMain函数:


- 初始化UIApplication单例对象

- 初始化AppDelegate对象,并设为UIApplication对象的代理

- 检查Info.plist设置的xib文件是否有效,如果有则解冻Nib文件并设置outlets,创建显示key window、rootViewController、与rootViewController关联的根view(没有关联则看rootViewController同名的xib),否则launch之后由程序员手动加载。

- 建立一个主事件循环,其中包含UIApplication的Runloop来开始处理事件。


UIApplication:


1、通过window管理视图;

2、发送Runloop封装好的control消息给target;

3、处理URL,应用图标警告,联网状态,状态栏,远程事件等。


AppDelegate:


管理UIApplication生命周期和应用的五种状态(notRunning/inactive/active/background/suspend)。


Key Window:


1、显示view;

2、管理rootViewcontroller生命周期;

3、发送UIApplication传来的事件消息给view。


rootViewController:


1、管理view(view生命周期;view的数据源/代理;view与superView之间事件响应nextResponder的“备胎”);

2、界面跳转与传值;

3、状态栏,屏幕旋转。


view:


1、通过作为CALayer的代理,管理layer的渲染(顺序大概是先更新约束,再layout再display)和动画(默认layer的属性可动画,view默认禁止,在UIView的block分类方法里才打开动画)。layer是RGBA纹理,通过和mask位图(含alpha属性)关联将合成后的layer纹理填充在像素点内,GPU每1/60秒将计算出的纹理display在像素点中。

2、布局子控件(屏幕旋转或者子视图布局变动时,view会重新布局)。

3、事件响应:event和guesture。


插播控制器生命周期


runloop:


1、(要让马儿跑)通过do-while死循环让程序持续运行:接收用户输入,调度处理事件时间。

2、(要让马儿少吃草)通过mach_msg()让runloop没事时进入trap状态,节省CPU资源。


关于程序启动原理以及各个控件的资料,已经有太多资料介绍,平时我们也经常接触经常用到,但关于Runloop的资料,官方文档总是太过简练,网上资源说法也不太统一,只能从CFRunLoopRef开源代码着手,试着学习总结下。(NSRunloop是对CFRunloopRef的面向对象封装,但是不是线程安全)。


--一--Runloop


1、与线程和自动释放池相关:


2、CFRunLoopRef构造:数据结构;创建与退出;mode切换和item依赖;Runloop启动

- CFRunLoopModeRef:数据结构(与CFRunLoopRef放一起了);创建;类型;

modeItems:- CFRunLoopSourceRef:数据结构(source0/source1);

- source0 :

- source1 :

- CFRunLoopTimerRef:数据结构;创建与生效;相关类型(GCD的timer与CADisplayLink)

- CFRunLoopObserverRef:数据结构;创建与添加;监听的状态;


3、Runloop内部逻辑:关键在两个判断点(是否睡觉,是否退出)

- 代码实现:

- 函数作用栈显示:


4、Runloop本质:mach port和mach_msg()。


5、如何处理事件:

- 界面刷新:

- 手势识别:

- GCD任务:

- timer:(与CADisplayLink)

- 网络请求:


6、应用:

- 滑动与图片刷新;

- 常驻子线程,保持子线程一直处理事件


Runloop


1、与线程和自动释放池相关:


Runloop的寄生于线程:一个线程只能有唯一对应的runloop;但这个根runloop里可以嵌套子runloops;

自动释放池寄生于Runloop:程序启动后,主线程注册了两个Observer监听runloop的进出与睡觉。一个最高优先级OB监测Entry状态;一个最低优先级OB监听BeforeWaiting状态和Exit状态。


线程(创建)-->runloop将进入-->最高优先级OB创建释放池-->runloop将睡-->最低优先级OB销毁旧池创建新池-->runloop将退出-->最低优先级OB销毁新池-->线程(销毁)


2、CFRunLoopRef构造:


  • 数据结构:


// runloop数据结构

struct __CFRunLoopMode {

CFStringRef _name; // Mode名字,

CFMutableSetRef _sources0; // Set<CFRunLoopSourceRef>

CFMutableSetRef _sources1; // Set<CFRunLoopSourceRef>

CFMutableArrayRef _observers; // Array<CFRunLoopObserverRef>

CFMutableArrayRef _timers; // Array<CFRunLoopTimerRef>

...

};

// mode数据结构

struct __CFRunLoop {

CFMutableSetRef _commonModes; // Set<CFStringRef>

CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>

CFRunLoopModeRef _currentMode; // Current Runloop Mode

CFMutableSetRef _modes; // Set<CFRunLoopModeRef>

...

};


  • 创建与退出:mode切换和item依赖


a 主线程的runloop自动创建,子线程的runloop默认不创建(在子线程中调用NSRunLoop *runloop = [NSRunLoop currentRunLoop];

获取RunLoop对象的时候,就会创建RunLoop);


b runloop退出的条件:app退出;线程关闭;设置最大时间到期;modeItem为空;


c 同一时间一个runloop只能在一个mode,切换mode只能退出runloop,再重进指定mode(隔离modeItems使之互不干扰);


d 一个item可以加到不同mode;一个mode被标记到commonModes里(这样runloop不用切换mode)。


  • 启动Runloop:


// 用DefaultMode启动

void CFRunLoopRun(void) {

CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);

}


// 用指定的Mode启动,允许设置RunLoop最大时间(假无限循环),执行完毕是否退出

int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {

return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);

}


  • CFRunLoopModeRef:


数据结构(见上);


创建添加:runloop自动创建对应的mode;mode只能添加不能删除


// 添加mode

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);


  • 类型:


1. kCFRunLoopDefaultMode: 默认 mode,通常主线程在这个 Mode 下运行。


2. UITrackingRunLoopMode: 追踪mode,保证Scrollview滑动顺畅不受其他 mode 影响。


3. UIInitializationRunLoopMode: 启动程序后的过渡mode,启动完成后就不再使用。


4: GSEventReceiveRunLoopMode: Graphic相关事件的mode,通常用不到。


5: kCFRunLoopCommonModes: 占位mode,作为标记DefaultMode和CommonMode用。


  • modeItems:


// 添加移除item的函数(参数:添加/移除哪个item到哪个runloop的哪个mode下)

CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);


CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);


CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);


CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);


CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);


CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);


  • A-- CFRunLoopSourceRef:事件来源


按照官方文档CFRunLoopSourceRef为3类,但数据结构只有两类(???)

Port-Based Sources:与内核端口相关

Custom Input Sources:与自定义source相关

Cocoa Perform Selector Sources:与PerformSEL方法相关)


  • 数据结构(source0/source1);


// source0 (manual): order(优先级),callout(回调函数)

CFRunLoopSource {order =..., {callout =... }}


// source1 (mach port):order(优先级),port:(端口), callout(回调函数)

CFRunLoopSource {order = ..., {port = ..., callout =...}


source0:event事件,只含有回调,需要标记待处理(signal),然后手动将runloop唤醒(wakeup);

source1 :包含一个 mach_port 和一个回调,被用于通过内核和其他线程发送的消息,能主动唤醒runloop。


  • B-- CFRunLoopTimerRef:系统内“定时闹钟”


NSTimer和performSEL方法实际上是对CFRunloopTimerRef的封装;runloop启动时设置的最大超时时间实际上是GCD的dispatch_source_t类型。


  • 数据结构:


// Timer:interval:(闹钟间隔), tolerance:(延期时间容忍度),callout(回调函数)

CFRunLoopTimer {firing =..., interval = ...,tolerance = ...,next fire date = ...,callout = ...}


创建与生效;


//NSTimer:

// 创建一个定时器(需要手动加到runloop的mode中)

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;


// 默认已经添加到主线程的runLoop的DefaultMode中

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;


// performSEL方法

// 内部会创建一个Timer到当前线程的runloop中(如果当前线程没runloop则方法无效;performSelector:onThread: 方法放到指定线程runloop中)

- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay


  • 相关类型(GCD的timer与CADisplayLink)


GCD的timer:


dispatch_source_t 类型,可以精确的参数,不用以来runloop和mode,性能消耗更小。


dispatch_source_set_timer(dispatch_source_t source, // 定时器对象

dispatch_time_t start, // 定时器开始执行的时间

uint64_t interval, // 定时器的间隔时间

uint64_t leeway // 定时器的精度

);


CADisplayLink :


Timer的tolerance表示最大延期时间,如果因为阻塞错过了这个时间精度,这个时间点的回调也会跳过去,不会延后执行。


CADisplayLink 是一个和屏幕刷新率一致的定时器,如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似,只是没有tolerance容忍时间),造成界面卡顿的感觉。


C--CFRunLoopObserverRef:监听runloop状态,接收回调信息(常见于自动释放池创建销毁)


数据结构:


// Observer:order(优先级),ativity(监听状态),callout(回调函数)

CFRunLoopObserver {order = ..., activities = ..., callout = ...}


创建与添加;


// 第一个参数用于分配该observer对象的内存空间

// 第二个参数用以设置该observer监听什么状态

// 第三个参数用于标识该observer是在第一次进入run loop时执行还是每次进入run loop处理时均执行

// 第四个参数用于设置该observer的优先级,一般为0

// 第五个参数用于设置该observer的回调函数

// 第六个参数observer的运行状态 

CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

// 执行代码

}


监听的状态;


typedef 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

};


3、Runloop内部逻辑:关键在两个判断点(是否睡觉,是否退出)


代码实现:


// RunLoop的实现

int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {


// 0.1 根据modeName找到对应mode

CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);

// 0.2 如果mode里没有source/timer/observer, 直接返回。

if (__CFRunLoopModeIsEmpty(currentMode)) return;


// 1.1 通知 Observers: RunLoop 即将进入 loop。---(OB会创建释放池)

__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);


// 1.2 内部函数,进入loop

__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {


Boolean sourceHandledThisLoop = NO;

int retVal = 0;

do {


// 2.1 通知 Observers: RunLoop 即将触发 Timer 回调。

__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);

// 2.2 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。

__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);

// 执行被加入的block

__CFRunLoopDoBlocks(runloop, currentMode);


// 2.3 RunLoop 触发 Source0 (非port) 回调。

sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);

// 执行被加入的block

__CFRunLoopDoBlocks(runloop, currentMode);


// 2.4 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。

if (__Source0DidDispatchPortLastTime) {

Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)

if (hasMsg) goto handle_msg;

}


// 3.1 如果没有待处理消息,通知 Observers: RunLoop 的线程即将进入休眠(sleep)。--- (OB会销毁释放池并建立新释放池)

if (!sourceHandledThisLoop) {

__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);

}


// 3.2. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。

// - 一个基于 port 的Source1 的事件。

// - 一个 Timer 到时间了

// - RunLoop 启动时设置的最大超时时间到了

// - 被手动唤醒

__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {

mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg

}


// 3.3. 被唤醒,通知 Observers: RunLoop 的线程刚刚被唤醒了。

__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);


// 4.0 处理消息。

handle_msg:


// 4.1 如果消息是Timer类型,触发这个Timer的回调。

if (msg_is_timer) {

__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())

}


// 4.2 如果消息是dispatch到main_queue的block,执行block。

else if (msg_is_dispatch) {

__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);

}


// 4.3 如果消息是Source1类型,处理这个事件

else {

CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);

sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);

if (sourceHandledThisLoop) {

mach_msg(reply, MACH_SEND_MSG, reply);

}

}


// 执行加入到Loop的block

__CFRunLoopDoBlocks(runloop, currentMode);



// 5.1 如果处理事件完毕,启动Runloop时设置参数为一次性执行,设置while参数退出Runloop

if (sourceHandledThisLoop && stopAfterHandle) {

retVal = kCFRunLoopRunHandledSource;

// 5.2 如果启动Runloop时设置的最大运转时间到期,设置while参数退出Runloop

} else if (timeout) {

retVal = kCFRunLoopRunTimedOut;

// 5.3 如果启动Runloop被外部调用强制停止,设置while参数退出Runloop

} else if (__CFRunLoopIsStopped(runloop)) {

retVal = kCFRunLoopRunStopped;

// 5.4 如果启动Runloop的modeItems为空,设置while参数退出Runloop

} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {

retVal = kCFRunLoopRunFinished;

}


// 5.5 如果没超时,mode里没空,loop也没被停止,那继续loop,回到第2步循环。

} while (retVal == 0);

}


// 6. 如果第6步判断后loop退出,通知 Observers: RunLoop 退出。--- (OB会销毁新释放池)

__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

}


函数作用栈显示:


{

// 1.1 通知Observers,即将进入RunLoop

// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);

do {


// 2.1 通知 Observers: 即将触发 Timer 回调。

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);

// 2.2 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);

// 执行Block

__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);


// 2.3 触发 Source0 (非基于port的) 回调。

__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);

// 执行Block

__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);


// 3.1 通知Observers,即将进入休眠

// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);


// 3.2 sleep to wait msg.

mach_msg() -> mach_msg_trap();


// 3.3 通知Observers,线程被唤醒

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);


// 4.1 如果是被Timer唤醒的,回调Timer

__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);


// 4.2 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block

__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);


// 4.3 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件

__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);


// 5. 退出判断函数调用栈无显示

} while (...);


// 6. 通知Observers,即将退出RunLoop

// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);

}


一步一步写具体的实现逻辑过于繁琐不便理解,按Runloop状态大致分为:


1- Entry:通知OB(创建pool);

2- 执行阶段:按顺序通知OB并执行timer,source0;若有source1执行source1;

3- 休眠阶段:利用mach_msg判断进入休眠,通知OB(pool的销毁重建);被消息唤醒通知OB;

4- 执行阶段:按消息类型处理事件;

5- 判断退出条件:如果符合退出条件(一次性执行,超时,强制停止,modeItem为空)则退出,否则回到第2阶段;

6- Exit:通知OB(销毁pool)。


4、Runloop本质:mach port和mach_msg()。


Mach是XNU的内核,进程、线程和虚拟内存等对象通过端口发消息进行通信,Runloop通过mach_msg()函数发送消息,如果没有port 消息,内核会将线程置于等待状态 mach_msg_trap() 。如果有消息,判断消息类型处理事件,并通过modeItem的callback回调(处理事件的具体执行是在DoBlock里还是在回调里目前我还不太明白???)。


Runloop有两个关键判断点,一个是通过msg决定Runloop是否等待,一个是通过判断退出条件来决定Runloop是否循环。


5、如何处理事件:


  • 界面刷新:


当UI改变( Frame变化、 UIView/CALayer 的继承结构变化等)时,或手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理。


苹果注册了一个用来监听BeforeWaiting和Exit的Observer,在它的回调函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。


  • 事件响应:


当一个硬件事件(触摸/锁屏/摇晃/加速等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收, 随后由mach port 转发给需要的App进程。


苹果注册了一个 Source1 (基于 mach port 的) 来接收系统事件,通过回调函数触发Sourece0(所以UIEvent实际上是基于Source0的),调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。


_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。


  • 手势识别:


如果上一步的 _UIApplicationHandleEventQueue() 识别到是一个guesture手势,会调用Cancel方法将当前的touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。


苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,其回调函数为 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。


当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。


  • GCD任务:


当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调里执行这个 block。Runloop只处理主线程的block,dispatch 到其他线程仍然是由 libDispatch 处理的。


  • timer:(见上modeItem部分)


  • 网络请求:


关于网络请求的接口:最底层是CFSocket层,然后是CFNetwork将其封装,然后是NSURLConnection对CFNetwork进行面向对象的封装,NSURLSession 是 iOS7 中新增的接口,也用到NSURLConnection的loader线程。所以还是以NSURLConnection为例。


当开始网络传输时,NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。



frameborder="0" allowtransparency="true" scrolling="no" vspace="0" hspace="0" style="margin: 0px; padding: 0px; position: static; display: block; border-style: none; vertical-align: baseline; width: 710px; height: 112px;">



6、应用:


  • 滑动与图片刷新;


当tableview的cell上有需要从网络获取的图片的时候,滚动tableView,异步线程会去加载图片,加载完成后主线程就会设置cell的图片,但是会造成卡顿。可以让设置图片的任务在CFRunLoopDefaultMode下进行,当滚动tableView的时候,RunLoop是在 UITrackingRunLoopMode 下进行,不去设置图片,而是当停止的时候,再去设置图片。


- (void)viewDidLoad {

[super viewDidLoad];

// 只在NSDefaultRunLoopMode下执行(刷新图片)

[self.myImageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@""] afterDelay:ti inModes:@[NSDefaultRunLoopMode]]; 

}


  • 常驻子线程,保持子线程一直处理事件


为了保证线程长期运转,可以在子线程中加入RunLoop,并且给Runloop设置item,防止Runloop自动退出。


+ (void)networkRequestThreadEntryPoint:(id)__unused object {

@autoreleasepool {

[[NSThread currentThread] setName:@"AFNetworking"];

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

[runLoop run];

}

}


+ (NSThread *)networkRequestThread {

static NSThread *_networkRequestThread = nil;

static dispatch_once_t oncePredicate;

dispatch_once(&oncePredicate, ^{

_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];

[_networkRequestThread start];

});

return _networkRequestThread;

}

- (void)start {

[self.lock lock];

if ([self isCancelled]) {

[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];

} else if ([self isReady]) {

self.state = AFOperationExecutingState;

[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];

}

[self.lock unlock];

}


这是一篇综合官方文档、绘制像素到屏幕上、View-Layer 协作、sunnyxx关于runloop的线下视频、深入理解RunLoop后加上自己的个人总结,各个资料有些说法都有差异,自己有整理有验证,也还存有疑惑,放上来希望得到指正。

学习iOS开发一般都是从UI开始的,从只知道从IB拖控件,到知道怎么在方法里写代码,然后会显示什么样的视图,产生什么样的事件,等等。其实程序从启动开始,一直都是按照苹果封装好的代码运行着,暴露的一些属性和方法作为接口,是让我们在给定的方法里写代码实现自定义功能,做出各种各样的应用。这些方法的调用顺序最为关键,熟悉了程序运转和方法调用的顺序,才可以更好地操控程序和代码,尽量避免Xcode不报错又实现不了功能的BUG。从Xcode的线程函数调用栈可以看到一些方法调用顺序。


--零--从程序启动开始到view显示:


start->(加载framework,动态静态链接库,启动图片,Info.plist,pch等)->main函数->UIApplicationMain函数:


- 初始化UIApplication单例对象

- 初始化AppDelegate对象,并设为UIApplication对象的代理

- 检查Info.plist设置的xib文件是否有效,如果有则解冻Nib文件并设置outlets,创建显示key window、rootViewController、与rootViewController关联的根view(没有关联则看rootViewController同名的xib),否则launch之后由程序员手动加载。

- 建立一个主事件循环,其中包含UIApplication的Runloop来开始处理事件。


UIApplication:


1、通过window管理视图;

2、发送Runloop封装好的control消息给target;

3、处理URL,应用图标警告,联网状态,状态栏,远程事件等。


AppDelegate:


管理UIApplication生命周期和应用的五种状态(notRunning/inactive/active/background/suspend)。


Key Window:


1、显示view;

2、管理rootViewcontroller生命周期;

3、发送UIApplication传来的事件消息给view。


rootViewController:


1、管理view(view生命周期;view的数据源/代理;view与superView之间事件响应nextResponder的“备胎”);

2、界面跳转与传值;

3、状态栏,屏幕旋转。


view:


1、通过作为CALayer的代理,管理layer的渲染(顺序大概是先更新约束,再layout再display)和动画(默认layer的属性可动画,view默认禁止,在UIView的block分类方法里才打开动画)。layer是RGBA纹理,通过和mask位图(含alpha属性)关联将合成后的layer纹理填充在像素点内,GPU每1/60秒将计算出的纹理display在像素点中。

2、布局子控件(屏幕旋转或者子视图布局变动时,view会重新布局)。

3、事件响应:event和guesture。


插播控制器生命周期


runloop:


1、(要让马儿跑)通过do-while死循环让程序持续运行:接收用户输入,调度处理事件时间。

2、(要让马儿少吃草)通过mach_msg()让runloop没事时进入trap状态,节省CPU资源。


关于程序启动原理以及各个控件的资料,已经有太多资料介绍,平时我们也经常接触经常用到,但关于Runloop的资料,官方文档总是太过简练,网上资源说法也不太统一,只能从CFRunLoopRef开源代码着手,试着学习总结下。(NSRunloop是对CFRunloopRef的面向对象封装,但是不是线程安全)。


--一--Runloop


1、与线程和自动释放池相关:


2、CFRunLoopRef构造:数据结构;创建与退出;mode切换和item依赖;Runloop启动

- CFRunLoopModeRef:数据结构(与CFRunLoopRef放一起了);创建;类型;

modeItems:- CFRunLoopSourceRef:数据结构(source0/source1);

- source0 :

- source1 :

- CFRunLoopTimerRef:数据结构;创建与生效;相关类型(GCD的timer与CADisplayLink)

- CFRunLoopObserverRef:数据结构;创建与添加;监听的状态;


3、Runloop内部逻辑:关键在两个判断点(是否睡觉,是否退出)

- 代码实现:

- 函数作用栈显示:


4、Runloop本质:mach port和mach_msg()。


5、如何处理事件:

- 界面刷新:

- 手势识别:

- GCD任务:

- timer:(与CADisplayLink)

- 网络请求:


6、应用:

- 滑动与图片刷新;

- 常驻子线程,保持子线程一直处理事件


Runloop


1、与线程和自动释放池相关:


Runloop的寄生于线程:一个线程只能有唯一对应的runloop;但这个根runloop里可以嵌套子runloops;

自动释放池寄生于Runloop:程序启动后,主线程注册了两个Observer监听runloop的进出与睡觉。一个最高优先级OB监测Entry状态;一个最低优先级OB监听BeforeWaiting状态和Exit状态。


线程(创建)-->runloop将进入-->最高优先级OB创建释放池-->runloop将睡-->最低优先级OB销毁旧池创建新池-->runloop将退出-->最低优先级OB销毁新池-->线程(销毁)


2、CFRunLoopRef构造:


  • 数据结构:


// runloop数据结构

struct __CFRunLoopMode {

CFStringRef _name; // Mode名字,

CFMutableSetRef _sources0; // Set<CFRunLoopSourceRef>

CFMutableSetRef _sources1; // Set<CFRunLoopSourceRef>

CFMutableArrayRef _observers; // Array<CFRunLoopObserverRef>

CFMutableArrayRef _timers; // Array<CFRunLoopTimerRef>

...

};

// mode数据结构

struct __CFRunLoop {

CFMutableSetRef _commonModes; // Set<CFStringRef>

CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>

CFRunLoopModeRef _currentMode; // Current Runloop Mode

CFMutableSetRef _modes; // Set<CFRunLoopModeRef>

...

};


  • 创建与退出:mode切换和item依赖


a 主线程的runloop自动创建,子线程的runloop默认不创建(在子线程中调用NSRunLoop *runloop = [NSRunLoop currentRunLoop];

获取RunLoop对象的时候,就会创建RunLoop);


b runloop退出的条件:app退出;线程关闭;设置最大时间到期;modeItem为空;


c 同一时间一个runloop只能在一个mode,切换mode只能退出runloop,再重进指定mode(隔离modeItems使之互不干扰);


d 一个item可以加到不同mode;一个mode被标记到commonModes里(这样runloop不用切换mode)。


  • 启动Runloop:


// 用DefaultMode启动

void CFRunLoopRun(void) {

CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);

}


// 用指定的Mode启动,允许设置RunLoop最大时间(假无限循环),执行完毕是否退出

int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {

return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);

}


  • CFRunLoopModeRef:


数据结构(见上);


创建添加:runloop自动创建对应的mode;mode只能添加不能删除


// 添加mode

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);


  • 类型:


1. kCFRunLoopDefaultMode: 默认 mode,通常主线程在这个 Mode 下运行。


2. UITrackingRunLoopMode: 追踪mode,保证Scrollview滑动顺畅不受其他 mode 影响。


3. UIInitializationRunLoopMode: 启动程序后的过渡mode,启动完成后就不再使用。


4: GSEventReceiveRunLoopMode: Graphic相关事件的mode,通常用不到。


5: kCFRunLoopCommonModes: 占位mode,作为标记DefaultMode和CommonMode用。


  • modeItems:


// 添加移除item的函数(参数:添加/移除哪个item到哪个runloop的哪个mode下)

CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);


CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);


CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);


CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);


CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);


CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);


  • A-- CFRunLoopSourceRef:事件来源


按照官方文档CFRunLoopSourceRef为3类,但数据结构只有两类(???)

Port-Based Sources:与内核端口相关

Custom Input Sources:与自定义source相关

Cocoa Perform Selector Sources:与PerformSEL方法相关)


  • 数据结构(source0/source1);


// source0 (manual): order(优先级),callout(回调函数)

CFRunLoopSource {order =..., {callout =... }}


// source1 (mach port):order(优先级),port:(端口), callout(回调函数)

CFRunLoopSource {order = ..., {port = ..., callout =...}


source0:event事件,只含有回调,需要标记待处理(signal),然后手动将runloop唤醒(wakeup);

source1 :包含一个 mach_port 和一个回调,被用于通过内核和其他线程发送的消息,能主动唤醒runloop。


  • B-- CFRunLoopTimerRef:系统内“定时闹钟”


NSTimer和performSEL方法实际上是对CFRunloopTimerRef的封装;runloop启动时设置的最大超时时间实际上是GCD的dispatch_source_t类型。


  • 数据结构:


// Timer:interval:(闹钟间隔), tolerance:(延期时间容忍度),callout(回调函数)

CFRunLoopTimer {firing =..., interval = ...,tolerance = ...,next fire date = ...,callout = ...}


创建与生效;


//NSTimer:

// 创建一个定时器(需要手动加到runloop的mode中)

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;


// 默认已经添加到主线程的runLoop的DefaultMode中

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;


// performSEL方法

// 内部会创建一个Timer到当前线程的runloop中(如果当前线程没runloop则方法无效;performSelector:onThread: 方法放到指定线程runloop中)

- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay


  • 相关类型(GCD的timer与CADisplayLink)


GCD的timer:


dispatch_source_t 类型,可以精确的参数,不用以来runloop和mode,性能消耗更小。


dispatch_source_set_timer(dispatch_source_t source, // 定时器对象

dispatch_time_t start, // 定时器开始执行的时间

uint64_t interval, // 定时器的间隔时间

uint64_t leeway // 定时器的精度

);


CADisplayLink :


Timer的tolerance表示最大延期时间,如果因为阻塞错过了这个时间精度,这个时间点的回调也会跳过去,不会延后执行。


CADisplayLink 是一个和屏幕刷新率一致的定时器,如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似,只是没有tolerance容忍时间),造成界面卡顿的感觉。


C--CFRunLoopObserverRef:监听runloop状态,接收回调信息(常见于自动释放池创建销毁)


数据结构:


// Observer:order(优先级),ativity(监听状态),callout(回调函数)

CFRunLoopObserver {order = ..., activities = ..., callout = ...}


创建与添加;


// 第一个参数用于分配该observer对象的内存空间

// 第二个参数用以设置该observer监听什么状态

// 第三个参数用于标识该observer是在第一次进入run loop时执行还是每次进入run loop处理时均执行

// 第四个参数用于设置该observer的优先级,一般为0

// 第五个参数用于设置该observer的回调函数

// 第六个参数observer的运行状态 

CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

// 执行代码

}


监听的状态;


typedef 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

};


3、Runloop内部逻辑:关键在两个判断点(是否睡觉,是否退出)


代码实现:


// RunLoop的实现

int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {


// 0.1 根据modeName找到对应mode

CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);

// 0.2 如果mode里没有source/timer/observer, 直接返回。

if (__CFRunLoopModeIsEmpty(currentMode)) return;


// 1.1 通知 Observers: RunLoop 即将进入 loop。---(OB会创建释放池)

__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);


// 1.2 内部函数,进入loop

__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {


Boolean sourceHandledThisLoop = NO;

int retVal = 0;

do {


// 2.1 通知 Observers: RunLoop 即将触发 Timer 回调。

__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);

// 2.2 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。

__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);

// 执行被加入的block

__CFRunLoopDoBlocks(runloop, currentMode);


// 2.3 RunLoop 触发 Source0 (非port) 回调。

sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);

// 执行被加入的block

__CFRunLoopDoBlocks(runloop, currentMode);


// 2.4 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。

if (__Source0DidDispatchPortLastTime) {

Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)

if (hasMsg) goto handle_msg;

}


// 3.1 如果没有待处理消息,通知 Observers: RunLoop 的线程即将进入休眠(sleep)。--- (OB会销毁释放池并建立新释放池)

if (!sourceHandledThisLoop) {

__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);

}


// 3.2. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。

// - 一个基于 port 的Source1 的事件。

// - 一个 Timer 到时间了

// - RunLoop 启动时设置的最大超时时间到了

// - 被手动唤醒

__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {

mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg

}


// 3.3. 被唤醒,通知 Observers: RunLoop 的线程刚刚被唤醒了。

__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);


// 4.0 处理消息。

handle_msg:


// 4.1 如果消息是Timer类型,触发这个Timer的回调。

if (msg_is_timer) {

__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())

}


// 4.2 如果消息是dispatch到main_queue的block,执行block。

else if (msg_is_dispatch) {

__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);

}


// 4.3 如果消息是Source1类型,处理这个事件

else {

CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);

sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);

if (sourceHandledThisLoop) {

mach_msg(reply, MACH_SEND_MSG, reply);

}

}


// 执行加入到Loop的block

__CFRunLoopDoBlocks(runloop, currentMode);



// 5.1 如果处理事件完毕,启动Runloop时设置参数为一次性执行,设置while参数退出Runloop

if (sourceHandledThisLoop && stopAfterHandle) {

retVal = kCFRunLoopRunHandledSource;

// 5.2 如果启动Runloop时设置的最大运转时间到期,设置while参数退出Runloop

} else if (timeout) {

retVal = kCFRunLoopRunTimedOut;

// 5.3 如果启动Runloop被外部调用强制停止,设置while参数退出Runloop

} else if (__CFRunLoopIsStopped(runloop)) {

retVal = kCFRunLoopRunStopped;

// 5.4 如果启动Runloop的modeItems为空,设置while参数退出Runloop

} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {

retVal = kCFRunLoopRunFinished;

}


// 5.5 如果没超时,mode里没空,loop也没被停止,那继续loop,回到第2步循环。

} while (retVal == 0);

}


// 6. 如果第6步判断后loop退出,通知 Observers: RunLoop 退出。--- (OB会销毁新释放池)

__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

}


函数作用栈显示:


{

// 1.1 通知Observers,即将进入RunLoop

// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);

do {


// 2.1 通知 Observers: 即将触发 Timer 回调。

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);

// 2.2 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);

// 执行Block

__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);


// 2.3 触发 Source0 (非基于port的) 回调。

__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);

// 执行Block

__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);


// 3.1 通知Observers,即将进入休眠

// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);


// 3.2 sleep to wait msg.

mach_msg() -> mach_msg_trap();


// 3.3 通知Observers,线程被唤醒

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);


// 4.1 如果是被Timer唤醒的,回调Timer

__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);


// 4.2 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block

__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);


// 4.3 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件

__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);


// 5. 退出判断函数调用栈无显示

} while (...);


// 6. 通知Observers,即将退出RunLoop

// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);

}


一步一步写具体的实现逻辑过于繁琐不便理解,按Runloop状态大致分为:


1- Entry:通知OB(创建pool);

2- 执行阶段:按顺序通知OB并执行timer,source0;若有source1执行source1;

3- 休眠阶段:利用mach_msg判断进入休眠,通知OB(pool的销毁重建);被消息唤醒通知OB;

4- 执行阶段:按消息类型处理事件;

5- 判断退出条件:如果符合退出条件(一次性执行,超时,强制停止,modeItem为空)则退出,否则回到第2阶段;

6- Exit:通知OB(销毁pool)。


4、Runloop本质:mach port和mach_msg()。


Mach是XNU的内核,进程、线程和虚拟内存等对象通过端口发消息进行通信,Runloop通过mach_msg()函数发送消息,如果没有port 消息,内核会将线程置于等待状态 mach_msg_trap() 。如果有消息,判断消息类型处理事件,并通过modeItem的callback回调(处理事件的具体执行是在DoBlock里还是在回调里目前我还不太明白???)。


Runloop有两个关键判断点,一个是通过msg决定Runloop是否等待,一个是通过判断退出条件来决定Runloop是否循环。


5、如何处理事件:


  • 界面刷新:


当UI改变( Frame变化、 UIView/CALayer 的继承结构变化等)时,或手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理。


苹果注册了一个用来监听BeforeWaiting和Exit的Observer,在它的回调函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。


  • 事件响应:


当一个硬件事件(触摸/锁屏/摇晃/加速等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收, 随后由mach port 转发给需要的App进程。


苹果注册了一个 Source1 (基于 mach port 的) 来接收系统事件,通过回调函数触发Sourece0(所以UIEvent实际上是基于Source0的),调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。


_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。


  • 手势识别:


如果上一步的 _UIApplicationHandleEventQueue() 识别到是一个guesture手势,会调用Cancel方法将当前的touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。


苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,其回调函数为 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。


当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。


  • GCD任务:


当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调里执行这个 block。Runloop只处理主线程的block,dispatch 到其他线程仍然是由 libDispatch 处理的。


  • timer:(见上modeItem部分)


  • 网络请求:


关于网络请求的接口:最底层是CFSocket层,然后是CFNetwork将其封装,然后是NSURLConnection对CFNetwork进行面向对象的封装,NSURLSession 是 iOS7 中新增的接口,也用到NSURLConnection的loader线程。所以还是以NSURLConnection为例。


当开始网络传输时,NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。



frameborder="0" allowtransparency="true" scrolling="no" vspace="0" hspace="0" style="margin: 0px; padding: 0px; position: static; display: block; border-style: none; vertical-align: baseline; width: 710px; height: 112px;">



6、应用:


  • 滑动与图片刷新;


当tableview的cell上有需要从网络获取的图片的时候,滚动tableView,异步线程会去加载图片,加载完成后主线程就会设置cell的图片,但是会造成卡顿。可以让设置图片的任务在CFRunLoopDefaultMode下进行,当滚动tableView的时候,RunLoop是在 UITrackingRunLoopMode 下进行,不去设置图片,而是当停止的时候,再去设置图片。


- (void)viewDidLoad {

[super viewDidLoad];

// 只在NSDefaultRunLoopMode下执行(刷新图片)

[self.myImageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@""] afterDelay:ti inModes:@[NSDefaultRunLoopMode]]; 

}


  • 常驻子线程,保持子线程一直处理事件


为了保证线程长期运转,可以在子线程中加入RunLoop,并且给Runloop设置item,防止Runloop自动退出。


+ (void)networkRequestThreadEntryPoint:(id)__unused object {

@autoreleasepool {

[[NSThread currentThread] setName:@"AFNetworking"];

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

[runLoop run];

}

}


+ (NSThread *)networkRequestThread {

static NSThread *_networkRequestThread = nil;

static dispatch_once_t oncePredicate;

dispatch_once(&oncePredicate, ^{

_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];

[_networkRequestThread start];

});

return _networkRequestThread;

}

- (void)start {

[self.lock lock];

if ([self isCancelled]) {

[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];

} else if ([self isReady]) {

self.state = AFOperationExecutingState;

[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];

}

[self.lock unlock];

}


这是一篇综合官方文档、绘制像素到屏幕上、View-Layer 协作、sunnyxx关于runloop的线下视频、深入理解RunLoop后加上自己的个人总结,各个资料有些说法都有差异,自己有整理有验证,也还存有疑惑,放上来希望得到指正。

内容概要:本文系统介绍了算术优化算法(AOA)的基本原理、核心思想及Python实现方法,并通过图像分割的实际案例展示了其应用价值。AOA是一种基于种群的元启发式算法,其核心思想来源于四则运算,利用乘除运算进行全局勘探,加减运算进行局部开发,通过数学优化器加速函数(MOA)和数学优化概率(MOP)动态控制搜索过程,在全局探索与局部开发之间实现平衡。文章详细解析了算法的初始化、勘探与开发阶段的更新策略,并提供了完整的Python代码实现,结合Rastrigin函数进行测试验证。进一步地,以Flask框架搭建前后端分离系统,将AOA应用于图像分割任务,展示了其在实际工程中的可行性与高效性。最后,通过收敛速度、寻优精度等指标评估算法性能,并提出自适应参数调整、模型优化和并行计算等改进策略。; 适合人群:具备一定Python编程基础和优化算法基础知识的高校学生、科研人员及工程技术人员,尤其适合从事人工智能、图像处理、智能优化等领域的从业者;; 使用场景及目标:①理解元启发式算法的设计思想与实现机制;②掌握AOA在函数优化、图像分割等实际问题中的建模与求解方法;③学习如何将优化算法集成到Web系统中实现工程化应用;④为算法性能评估与改进提供实践参考; 阅读建议:建议读者结合代码逐行调试,深入理解算法流程中MOA与MOP的作用机制,尝试在不同测试函数上运行算法以观察性能差异,并可进一步扩展图像分割模块,引入更复杂的预处理或后处理技术以提升分割效果。
<think>好的,我现在需要处理用户的问题,他想递归遍历指定文件夹及其子文件夹中的所有图片,并进行统一的重命名顺序处理。让我仔细看看用户的需求和之前的对话历史,确保我理解正确。 用户当前的路径是E:\Pycharm_demo\yolov5\VOCData\image_1,之前提供的代码只能处理单层目录,现在他希望包括子文件夹中的图片也纳入命名顺序。也就是说,所有图片,无论位于哪个子文件夹中,都需要被收集起来,统一编号,然后重命名。同时,用户可能希望保持原有的目录结构,只是修改文件名。 首先,我需要调整代码,使其能够递归遍历所有子文件夹。Python的os.walk函数可以实现这一点,它会遍历目录树下的所有文件和子目录。然后,收集所有图片文件的路径,并根据它们的相对路径进行排序,以确保处理顺序的一致性。 接下来是重命名的部分。原来的代码是在同一文件夹内进行重命名,但现在需要处理多个子文件夹中的文件。因此,需要将文件移动到新的统一目录,还是保持原有目录结构,只修改文件名?用户可能希望后者,即保持目录结构不变,只修改文件名,但所有图片按顺序统一命名,比如1.jpg, 2.jpg等,不管它们在哪个子文件夹里。 不过,如果所有图片都统一编号,可能会存在文件名冲突,比如两个不同子文件夹中的图片都被重命名为1.jpg,这会导致覆盖。因此,必须确保所有图片被收集到一个统一的列表中,按顺序编号,然后在各自原来的目录中进行重命名,这样每个文件在各自的目录中都是唯一的。或者,用户可能希望将所有图片集中到一个文件夹中,并按顺序命名。但根据用户的问题描述,他说“将子文件夹中的图片也纳入命名顺序处理”,可能是指保持原有目录结构,但所有图片统一编号,例如根目录和子目录中的图片按顺序依次编号为1.jpg, 2.jpg等,不管它们的位置。 但这样会有问题,因为不同子文件夹中的文件如果被重命名为相同的名字,会导致覆盖。因此,可能需要将所有图片移动到一个统一的目录中,并按顺序命名。或者,用户可能希望每个子文件夹中的图片独立编号,但根据用户的问题,他想要“统一命名顺序处理”,也就是所有图片不管在哪个子文件夹,都统一编号,比如1.jpg, 2.jpg,...,这样可能需要将所有图片移动到一个目标文件夹,否则不同子文件夹中的文件重命名后会有相同的名字,导致覆盖。 但用户可能不希望移动文件,而是保持原有的目录结构,只是统一编号。这种情况下,如果不同子文件夹中的文件被重命名为相同的名字,就会有问题。因此,可能需要用户澄清,但根据问题描述,用户可能希望将所有子文件夹中的图片统一处理,即所有图片按顺序编号,不管它们在哪个子文件夹中,但这样必须移动文件到一个目录,否则会有冲突。或者,用户可能希望在每个子文件夹内部独立编号,但问题描述是“统一命名顺序处理”,所以更可能是前者,即所有图片收集起来,统一编号,并可能移动到统一的目录,或者保持原目录结构但统一编号,但需要避免冲突。 不过,用户之前提供的代码是批量重命名一个文件夹中的图片,现在希望递归处理子文件夹,并且统一顺序处理。可能用户希望将所有图片(包括子文件夹中的)收集起来,统一编号,然后重命名,但保留原来的目录结构,每个文件在自己的目录中被重命名。但这样每个目录中的文件会被独立处理,比如每个子文件夹中的图片都从1开始编号,但用户可能想要全局的统一编号,即所有图片按顺序排号,不管在哪个子文件夹里,比如根目录中的图片是1.jpg,子文件夹中的第一个图片是2.jpg,以此类推。 这样的话,需要将所有图片路径收集起来,排序,然后按顺序编号,然后在各自原来的路径中重命名。比如,收集所有图片的路径,按自然顺序排序,然后依次为每个文件生成新的文件名(如1.jpg, 2.jpg等),并将它们重命名到原来的目录中。这样,每个文件在各自的目录中被重命名为全局唯一的编号,但文件仍然保留在原来的位置。但这样不同目录中的文件可能会有相同的名字,比如子文件夹A中的1.jpg和子文件夹B中的1.jpg,这会导致如果它们被移动到同一个目录下会有冲突,但用户可能不需要移动,只是每个文件在自己的目录中被重命名为全局唯一的编号,或者用户希望将所有图片都移动到根目录下并按顺序命名。 这时候需要进一步确认用户的需求,但根据问题描述,用户可能希望保持目录结构,但所有图片统一编号。因此,解决方案可能是遍历所有子文件夹中的图片,收集它们的路径,按某种顺序(如按目录结构排序)编号,然后在各自的目录中重命名为全局唯一的编号。但这样每个文件所在的目录不同,文件名相同不会有冲突,因为它们在不同目录中。例如,根目录下的1.jpg,子目录下的2.jpg,或者可能用户希望所有图片不管目录,统一编号,比如总共有100张图片,编号1到100,每个文件在各自的位置被重命名为1.jpg,2.jpg等,不管目录结构。但这样会导致不同目录中的文件可能被重命名为相同的名字,但实际在文件系统中是允许的,因为路径不同。 不过,用户可能更希望将所有图片收集到一个目标目录,并按顺序命名,但问题中用户的路径是E:\Pycharm_demo\yolov5\VOCData\image_1,可能希望处理该目录下的所有子文件夹中的图片,并按顺序重命名,可能保持目录结构,但每个文件在自己的目录中被重命名,或者将所有图片移动到image_1目录下,并统一编号。需要进一步分析。 原来的示例代码中,处理后的结果是所有文件在image_1目录下被重命名为1.jpg, 2.png等。现在用户希望子文件夹中的图片也被处理,可能希望将所有子文件夹中的图片都移动到image_1下并按顺序命名,或者保持子文件夹结构,但每个子文件夹中的图片独立编号。但根据用户的问题,“统一命名顺序处理”,可能是指所有图片,无论位于哪个子文件夹,都按照一个全局的顺序编号,比如1.jpg, 2.jpg,不管在哪个子文件夹里。但这样的话,如果两个不同的子文件夹中都有图片,重命名为相同的名字会导致覆盖,如果移动到一个目录的话。因此,必须明确如何处理。 可能的解决方案有两种: 1. 将所有子文件夹中的图片移动到根目录(image_1)下,并按顺序重命名。这样可以统一编号,但会改变文件的位置,可能破坏原有的目录结构,对于YOLOv5数据集来说,可能同时需要调整标注文件的位置,这可能不可取。 2. 保持原有目录结构,每个图片在自己的目录中被重命名为全局唯一的编号。例如,根目录下的图片是1.jpg,子文件夹A中的图片是2.jpg,子文件夹B中的图片是3.jpg,依此类推。这样,每个文件在自己的目录中被重命名,但全局编号是连续的。这种情况下,需要收集所有图片的路径,按某种顺序排序(例如,按目录层级和文件名排序),然后依次编号,并在各自的目录中进行重命名。这样不会导致文件名冲突,因为每个文件所在的目录不同,文件名可以相同,但用户可能希望每个文件有唯一的编号,无论目录如何。 不过,用户的问题可能更倾向于第二种情况:保持目录结构,但所有图片统一编号,例如1.jpg在根目录,子目录中的下一个图片是2.jpg,等等。这样每个图片在文件系统中的完整路径是唯一的,但文件名本身可能重复,但用户可能更关注全局唯一的编号,因此文件名可以重复,因为路径不同。或者用户可能希望文件名是全局唯一的,不管路径如何,这可能需要将所有文件移动到同一目录,但这样可能不符合用户的需求,特别是如果用户的数据集结构需要保持目录的话。 因此,正确的做法可能是遍历所有子文件夹中的图片,收集它们的绝对路径,按自然顺序排序(例如,按目录结构深度优先遍历的顺序),然后为每个文件生成一个全局唯一的序号,如1、2、3等,然后在各自的目录中将它们重命名为该序号加上扩展名。这样,每个文件在自己的目录中被重命名为唯一的序号,但不同目录中的文件可能有相同的文件名,例如: image_1/1.jpg image_1/subdir/2.jpg image_1/subdir/subsubdir/3.jpg 这样,每个文件在自己的目录中有一个唯一的文件名,但全局来看,序号是递增的。或者用户可能希望所有文件都按顺序命名,不管目录,这样可能会有重复的文件名,但在不同的目录中是可以存在的。 不过,用户的需求是“统一命名顺序处理”,可能希望所有图片,不管在哪个子文件夹中,都按照一个全局的顺序进行命名,例如总共有100张图片,全部命名为1.jpg到100.jpg,每个文件所在的目录可能不同,但文件名是唯一的。这种情况下,需要将文件收集起来,排序,然后为每个文件生成一个唯一的序号,并在其原始目录中重命名为该序号。例如: 原始结构: image_1/a.jpg image_1/subdir/b.jpg 处理后: image_1/1.jpg image_1/subdir/2.jpg 这样,每个文件被重命名为全局唯一的序号,但保留在原来的目录中。这样不会导致文件名冲突,因为它们在不同目录中,但全局序号是连续的。这可能符合用户的需求。 但用户可能希望将所有文件都放在同一个目录下,例如image_1,并且按顺序命名,不管原来的子文件夹结构。这可能更常见于数据集处理中,将所有图片集中到一个文件夹,方便后续处理。但用户的问题描述没有明确说明这一点,因此需要进一步考虑。 回到用户的问题,他说“递归遍历 E:\Pycharm_demo\yolov5\VOCData\image_1 文件夹及其中的子文件夹,并对所有图片进行统一命名顺序处理”。这里的“统一命名顺序处理”可能意味着所有图片,不管在哪个子文件夹中,都按照一个全局的顺序进行编号,例如1.jpg, 2.jpg等,不管它们位于哪个子目录中。在这种情况下,需要将所有图片收集起来,按一定顺序排序,然后统一编号,同时可能需要将它们移动到统一的目录,或者在原目录中重命名,但这样会有重复的文件名。 因此,可能的解决方案有两种: 1. 将所有图片移动到根目录(image_1)并按顺序命名。这样处理后,所有图片都在一个目录下,文件名唯一,但原来的子文件夹结构被破坏。这可能适用于用户不需要保留目录结构的情况。 2. 保持原有目录结构,每个图片在各自的目录中被重命名为全局唯一的序号,例如根目录中的图片是1.jpg,子目录中的下一个图片是2.jpg,依此类推。这样,每个文件在自己的目录中有唯一的文件名,全局序号递增,但不同目录中的文件名可能重复,但因为路径不同,所以不会冲突。 用户的需求可能更倾向于第一种,即所有图片被集中到根目录并按顺序命名,但需要明确。或者用户可能希望保持目录结构,但每个图片在全局范围内有一个唯一的编号,并保留在原来的位置。例如,处理后的文件可能分布在不同的子文件夹中,但每个文件的名字是唯一的,如1.jpg, 2.jpg等,无论它们的位置如何。这种情况下,需要确保在重命名时,不同目录中的文件不会出现相同的序号,因此需要将所有文件收集起来,按顺序编号,然后在各自的目录中重命名。 例如,收集所有图片的路径,按某种顺序排序(例如,按路径的字母顺序或自然排序),然后为每个文件分配一个全局唯一的序号,如1、2、3等,然后在各自的目录中重命名为该序号加上扩展名。这样,每个文件的名字在全局范围内是唯一的,并且保留在原来的目录中。例如: 原路径:image_1/subdir1/a.jpg → 重命名为image_1/subdir1/1.jpg 原路径:image_1/subdir2/b.jpg → 重命名为image_1/subdir2/2.jpg 原路径:image_1/c.jpg → 重命名为image_1/3.jpg 这种情况下,所有图片在各自的目录中被重命名为全局唯一的编号。这样处理的话,用户可能需要所有图片在全局有一个唯一的ID,不管目录如何,但文件名本身可能相同,但路径不同,所以实际是唯一的。不过,用户的问题可能希望文件名本身是唯一的,例如所有图片都命名为1.jpg, 2.jpg等,不管目录,这时候需要将所有图片移动到一个统一的目录中。 此时需要进一步分析用户的实际需求。根据用户提供的之前代码示例,处理后的结果是将所有图片放在同一个目录下,按顺序命名。因此,可能用户现在希望将所有子文件夹中的图片都移动到根目录(image_1)并按顺序命名,同时删除空的子文件夹。或者,用户希望保持目录结构,但为每个图片分配全局唯一的编号,并在原目录中重命名。 为了确认,我需要参考用户之前的代码。之前的代码示例中,函数batch_rename_yolo_images处理的是单个文件夹,将其中所有图片重命名为1.jpg, 2.png等。现在用户想要处理子文件夹中的图片,并统一命名顺序,可能希望将所有子文件夹中的图片都移动到根目录,并按顺序命名,或者保持结构但全局编号。 但用户的问题中提到“递归遍历”,所以可能希望处理所有子文件夹中的图片,但保持目录结构,同时为每个文件分配全局唯一的编号。例如,收集所有图片,按某种顺序排序,然后依次编号,并在原路径中重命名。例如: 原结构: image_1/a.jpg image_1/subdir1/b.jpg image_1/subdir2/c.jpg 处理后的结构: image_1/1.jpg image_1/subdir1/2.jpg image_1/subdir2/3.jpg 这样,每个图片在全局范围内有唯一的编号,但保留在原来的目录中。这种情况下,需要遍历所有子文件夹,收集所有图片路径,排序,然后按顺序重命名。 接下来,我需要考虑如何实现这一点。首先,使用os.walk遍历所有目录,收集所有图片文件的路径。然后,对这些路径进行自然排序。然后,逐个重命名为1.jpg, 2.jpg等,保持扩展名,并在原目录中重命名。 但需要注意的是,如果在不同目录中有同名的文件,比如subdir1中的1.jpg和subdir2中的2.jpg,这不会冲突,因为它们路径不同。但用户可能希望所有图片的文件名是全局唯一的,不管路径,所以需要将所有图片的文件名统一为递增的数字,无论路径如何。 因此,代码需要: 1. 递归遍历所有子文件夹,收集所有图片文件的路径。 2. 对这些路径进行排序(按路径字符串的自然排序或按修改时间等)。 3. 为每个文件分配一个全局唯一的序号。 4. 在原目录中将文件重命名为序号+扩展名。 这样,处理后的文件结构将保留原来的目录结构,但每个文件的名字是全局唯一的数字。 现在,如何实现这一点? 首先,收集所有图片文件: 使用os.walk遍历根目录,对于每个目录,获取所有符合扩展名的文件,并记录它们的完整路径。例如: valid_ext = {'.jpg', '.jpeg', '.png', '.bmp', '.webp'} all_images = [] for root, dirs, files in os.walk(folder_path): for file in files: ext = os.path.splitext(file)[1].lower() if ext in valid_ext: full_path = os.path.join(root, file) all_images.append(full_path) 然后,对all_images进行自然排序。自然排序可以通过拆分数字部分和非数字部分,例如使用正则表达式: import re def natural_sort_key(s): return [int(c) if c.isdigit() else c.lower() for c in re.split('(\d+)', s)] all_images.sort(key=lambda x: natural_sort_key(x)) 或者,可以按文件的修改时间排序,或者按路径的字母顺序。用户可能需要按路径的自然顺序排序,例如先处理根目录下的文件,再处理子目录中的文件,按目录层级排序。 然后,为每个文件分配一个序号,从1开始,逐个递增。例如: for index, old_path in enumerate(all_images, 1): dir_name = os.path.dirname(old_path) file_name = os.path.basename(old_path) ext = os.path.splitext(file_name)[1].lower() new_name = f"{index}{ext}" new_path = os.path.join(dir_name, new_name) os.rename(old_path, new_path) 这样,每个文件在原来的目录中被重命名为全局唯一的序号,例如1.jpg, 2.png等。 但需要注意,如果在同一个目录中有多个文件,它们会被重命名为不同的序号,例如目录中的三个文件会变成1.jpg, 2.png, 3.bmp,而另一个目录中的文件会从4.jpg开始。 这样处理后的文件名在全局范围内是唯一的,每个文件有唯一的序号,不管位于哪个目录中。这可能符合用户的需求,即“统一命名顺序处理”,将所有子文件夹中的图片纳入处理,并按顺序编号。 但用户可能需要确保在重命名时不会出现文件名冲突,例如,原目录中可能存在名为1.jpg的文件,这可能会被覆盖。因此,在重命名前需要检查目标路径是否存在,如果存在,则跳过或处理冲突。 因此,代码中需要添加防覆盖机制,例如: for index, old_path in enumerate(all_images, 1): dir_name = os.path.dirname(old_path) ext = os.path.splitext(old_path)[1].lower() new_name = f"{index}{ext}" new_path = os.path.join(dir_name, new_name) if os.path.exists(new_path): print(f"警告:跳过 {old_path},{new_name} 已存在") continue try: os.rename(old_path, new_path) print(f"重命名成功:{old_path} → {new_name}") except Exception as e: print(f"错误:处理 {old_path} 失败 - {str(e)}") 但这样处理时,如果原目录中已经有1.jpg,而新文件名也是1.jpg,就会冲突,可能覆盖原有文件。因此,需要确保目标文件名在同一个目录中不存在。但根据上面的逻辑,如果所有文件都是从旧文件名转换而来,而旧文件名已经被收集到列表中,那么可能在重命名时,如果旧文件名已经被处理过,可能会出现冲突。例如,如果有一个文件原来名为2.jpg,在处理时,当index为2时,新文件名是2.jpg,如果原文件已经是数字命名,可能覆盖自己。 因此,可能需要先将所有文件收集起来,然后统一处理,或者先复制到临时目录,再重命名。但这样会增加复杂度。另一种方法是先收集所有文件,然后按顺序处理,确保在重命名时不会覆盖尚未处理的文件。 例如,假设文件列表已经按顺序排好,处理时,按顺序将每个文件重命名为index,这样即使原文件名是数字,处理时按顺序逐个处理,不会出现覆盖的问题,因为每个文件在列表中是唯一的,处理后的文件名是新的唯一序号。 例如,假设原文件名为5.jpg,当处理到它时,可能它的index是10,因此会被重命名为10.jpg,而原目录中的其他文件已经被处理为1.jpg到9.jpg,所以不会冲突。 因此,只要文件列表的顺序正确,并且在处理时逐个重命名,覆盖的问题应该不会发生,因为原文件名可能已经被处理成其他名字,或者尚未处理。 但需要测试这种情况。例如,假设原目录中有文件1.jpg, 2.jpg,在处理时,收集到的列表按自然排序为1.jpg, 2.jpg,当处理第一个文件时,重命名为1.jpg,即原1.jpg变成1.jpg,这不会有变化。第二个文件原名为2.jpg,处理时重命名为2.jpg,也不变。因此,这种情况下,代码不会改变这些文件,但如果用户希望将所有文件重新编号,不管原来的名字,这可能不符合预期。 例如,用户可能希望不管原文件名是什么,都按顺序编号为1.jpg, 2.jpg等,即使原文件名已经是数字。例如,原文件名为5.jpg,处理后的名字是3.jpg(假设是第三个文件),这样就需要重命名。但按照上述代码,如果原文件名是5.jpg,它在列表中的位置可能较后,处理时会被赋予更大的序号,导致覆盖。 例如,假设原目录中有文件5.jpg,3.jpg,1.jpg,按自然排序后的顺序是1.jpg, 3.jpg, 5.jpg。处理时,第一个文件1.jpg被重命名为1.jpg(不变),第二个文件3.jpg被重命名为2.jpg,第三个文件5.jpg被重命名为3.jpg。这样处理是正确的,不会出现覆盖,因为处理顺序是按排序后的顺序,逐个处理,先处理1.jpg,然后3.jpg变成2.jpg,然后5.jpg变成3.jpg。此时,处理后的文件是1.jpg, 2.jpg, 3.jpg,这符合预期。 因此,代码是正确的,不会出现覆盖的问题,因为处理顺序是按原文件名的自然排序,然后逐个赋予递增的序号,因此在处理过程中,后续的文件名不会与已处理的文件名冲突,因为序号是递增的,且原文件可能已经被重命名。 但需要注意,如果原文件名中有数字,可能导致排序后的顺序与预期不同。例如,文件a1.jpg, a10.jpg, a2.jpg,按自然排序会排成a1, a2, a10,这样处理后的序号是正确的。因此,自然排序是必要的。 综上所述,解决方案是: 1. 使用os.walk递归收集所有图片文件路径。 2. 对路径进行自然排序。 3. 按顺序为每个文件分配全局唯一的序号。 4. 在原目录中将文件重命名为序号+扩展名,确保不覆盖现有文件。 现在,我需要将这些步骤转化为Python代码,并确保处理正确。 此外,需要考虑扩展名的大小写问题,例如.JPG和.jpg应统一处理,保留原扩展名但可能转换为小写。或者,保持原扩展名的大小写,但在代码中统一处理为小写进行比较,但保存时使用原扩展名。例如,文件IMAGE.JPG会被处理为1.JPG,而image.png处理为2.png。 用户之前的代码中,处理扩展名时使用的是小写,但保存时保留原扩展名。例如: ext = os.path.splitext(filename)[1].lower() 但新文件名使用原扩展名的大小写。或者,可能统一转换为小写。需要确认用户的需求。根据之前的示例,处理后的文件扩展名是小写,如将.JPEG保存为.jpeg,但可能用户希望保留原扩展名的大小写。例如,原文件是IMG_01.JPEG,处理后的文件是4.jpeg。或者,可能统一转换为小写扩展名。需要检查用户之前的代码。 在用户之前的代码中,处理扩展名的方式是: ext = os.path.splitext(filename)[1].lower() 然后新文件名是: new_name = f"{index}{ext}" 即扩展名被转换为小写。例如,原文件是IMG_01.JPEG,处理后为4.jpeg。这可能符合用户的需求,因为统一扩展名格式有助于后续处理。因此,在代码中,应将扩展名转换为小写。 现在,整合所有思路,编写代码: 首先,导入必要的模块: import os import re 然后,定义自然排序的函数: def natural_sort_key(s): return [int(c) if c.isdigit() else c.lower() for c in re.split('(\d+)', s)] 然后,递归收集所有图片文件: def batch_rename_yolo_images_recursive(folder_path): valid_ext = {'.jpg', '.jpeg', '.png', '.bmp', '.webp'} all_images = [] for root, dirs, files in os.walk(folder_path): for file in files: ext = os.path.splitext(file)[1].lower() if ext in valid_ext: full_path = os.path.join(root, file) all_images.append(full_path) # 自然排序 all_images.sort(key=lambda x: natural_sort_key(x)) # 重命名 for index, old_path in enumerate(all_images, 1): dir_name = os.path.dirname(old_path) file_name = os.path.basename(old_path) ext = os.path.splitext(file_name)[1].lower() new_name = f"{index}{ext}" new_path = os.path.join(dir_name, new_name) # 检查是否存在 if os.path.exists(new_path): print(f"警告:跳过 {old_path},{new_name} 已存在") continue try: os.rename(old_path, new_path) print(f"重命名成功:{old_path} → {new_name}") except Exception as e: print(f"错误:处理 {old_path} 失败 - {str(e)}") 这样,函数会递归收集所有图片,按自然排序后,逐个重命名为全局唯一的序号,保留在原目录中。 测试示例: 原结构: image_1/ a.jpg subdir1/ b.png subdir2/ c.jpeg 处理后: image_1/1.jpg image_1/subdir1/2.png image_1/subdir2/3.jpeg 这样,所有图片都被正确编号,不管目录层级。 但需要注意,如果不同子文件夹中的文件在排序后的列表中顺序交叉,例如: image_1/subdir1/a.jpg image_1/z.jpg 处理时,可能subdir1/a.jpg在z.jpg之前,因为自然排序中'subdir1/' 在 'z.jpg'之前。因此,收集到的文件顺序可能按目录结构排序,即先处理根目录下的文件,再处理子目录中的文件,按目录的层级顺序。这是os.walk的默认行为,即深度优先还是广度优先?在Python中,os.walk默认是深度优先,即先处理当前目录,然后递归处理子目录。因此,收集到的文件列表顺序是根目录下的文件先被处理,然后是子目录中的文件,按目录的字母顺序处理。 例如,根目录下的文件会在子目录中的文件之前被处理,因此他们的序号更小。这可能符合用户的预期,即根目录的文件先编号,然后是子目录中的文件。 如果需要调整排序顺序,可以自定义排序方式,例如按完整路径的自然排序: all_images.sort(key=lambda x: natural_sort_key(x)) 这将按完整路径的字符串自然排序,例如: E:\path\image_1\a.jpg E:\path\image_1\subdir1\b.jpg E:\path\image_1\subdir2\c.jpg 会按字母顺序排列,因此根目录下的文件先出现,然后是subdir1,再是subdir2中的文件。 这应该符合用户的预期。 现在,测试可能遇到的问题: 1. 文件在重命名时可能覆盖自身:例如,原文件名为1.jpg,在处理时会被重命名为1.jpg,这不会有变化。但如果原文件已经按顺序命名,用户可能希望重新编号所有文件,不管原来的名字。例如,原文件是1.jpg, 2.jpg,处理后还是1.jpg, 2.jpg,这没有改变。但用户可能希望将所有文件重新编号,不管原来的名字。例如,原文件名为existing_1.jpg,处理后变为1.jpg,原existing_2.jpg变为2.jpg,等等。因此,代码会正确处理这种情况,因为无论原文件名如何,都会被重新编号。 例如,原文件列表按自然排序后的顺序是: image_1/existing_1.jpg image_1/existing_2.jpg image_1/subdir/existing_3.jpg 处理后,这些文件会被重命名为1.jpg, 2.jpg, 3.jpg,无论它们的原文件名如何。 2. 扩展名处理:统一转换为小写,如.JPG变为.jpg。 3. 文件名冲突:例如,处理到某个文件时,目标文件名已经存在。例如,处理到第三个文件时,目标路径可能已被之前的文件占用。但由于处理是按顺序进行的,且每个新文件名是递增的,这种情况应该不会发生,除非原目录中已经有文件名为数字的文件。例如,原目录中有1.jpg,而处理后的第一个文件也被命名为1.jpg,但原1.jpg在文件列表中,会被处理为1.jpg,这样不会冲突,因为处理是按顺序的,原文件可能已经被重命名。或者,如果原文件名为5.jpg,在列表中的位置较后,处理时会被赋予更大的序号,因此不会冲突。 例如,原文件列表为: image_1/5.jpg image_1/3.jpg image_1/1.jpg 自然排序后顺序为1.jpg, 3.jpg, 5.jpg。处理时,1.jpg被重命名为1.jpg(不变),3.jpg被重命名为2.jpg,5.jpg被重命名为3.jpg。这样,原来的5.jpg变成3.jpg,不会与任何文件冲突。 因此,代码是安全的,不会出现覆盖问题。 最后,用户可能需要测试代码,确保符合预期。因此,在回答中需要提供代码,并说明其功能,递归处理子文件夹,统一编号,防覆盖机制,自然排序等。</think>根据您的需求,我将为您提供一套支持递归处理的增强版图片批量重命名方案。以下是基于前序方案优化的代码: ### Python递归重命名方案 (支持子文件夹) ```python import os import re def batch_rename_recursive(folder_path): """ 递归批量重命名函数(支持子文件夹) 功能特性: 1. 递归遍历所有子目录 2. 全局统一编号命名 3. 自动保留原始扩展名 4. 智能冲突检测机制 """ # 路径有效性验证(引用[5]的验证逻辑) if not os.path.exists(folder_path): raise FileNotFoundError(f"目标路径 {folder_path} 不存在") # 递归收集所有图片(改进引用[2]的文件过滤) valid_ext = {'.jpg', '.jpeg', '.png', '.bmp', '.webp'} image_paths = [] for root, dirs, files in os.walk(folder_path): for file in files: ext = os.path.splitext(file)[1].lower() if ext in valid_ext: image_paths.append(os.path.join(root, file)) # 自然排序算法优化(增强引用[3]的排序逻辑) def natural_sort_key(s): return [int(c) if c.isdigit() else c.lower() for c in re.split('(\d+)', s)] image_paths.sort(key=natural_sort_key) # 执行全局重命名 renamed_count = 0 for index, old_path in enumerate(image_paths, start=1): dir_path = os.path.dirname(old_path) ext = os.path.splitext(old_path)[1].lower() new_name = f"{index}{ext}" new_path = os.path.join(dir_path, new_name) # 增强型冲突检测(优化引用[4]的机制) if os.path.exists(new_path): print(f"⚠️ 冲突跳过:{os.path.basename(old_path)} → {new_name}") continue try: os.rename(old_path, new_path) renamed_count += 1 print(f"✅ 成功:{os.path.relpath(old_path, folder_path)} → {new_name}") except Exception as e: print(f"❌ 错误:处理 {os.path.basename(old_path)} 失败 - {str(e)}") print(f"\n处理完成:共重命名 {renamed_count}/{len(image_paths)} 个文件") # 使用示例(请替换实际路径) batch_rename_recursive(r"E:\Pycharm_demo\yolov5\VOCData\image_1") ``` ### 执行效果演示 **原始目录结构**: ``` image_1/ ├─ subfolder1/ │ ├─ photoA.jpg │ └─ screenshot.png ├─ image_2023.bmp └─ subfolder2/ └─ diagram.webp ``` **执行后结构**: ``` image_1/ ├─ subfolder1/ │ ├─ 1.jpg │ └─ 2.png ├─ 3.bmp └─ subfolder2/ └─ 4.webp ``` ### 核心改进点 1. **递归遍历机制** 采用`os.walk`实现深度优先遍历,确保处理所有层级的子文件夹(引用[1]的目录遍历方法) 2. **全局统一编号** 所有图片文件按路径自然排序后获得全局唯一序号,满足跨目录统一命名需求 3. **相对路径显示** 输出日志中显示相对路径,便于快速定位文件位置 4. **增强错误处理** - 增加重命名计数器统计成功率 - 采用图形化符号区分不同状态(✅/⚠️/❌) ### 高级特性 1. **智能排序算法** 采用正则表达式`(\d+)`拆分数字与非数字部分,正确处理复杂文件名如: - `IMG_001.jpg` → `1.jpg` - `photo99.png` → `2.png` - `screenshot100.jpg` → `3.jpg` 2. **跨平台兼容性** 路径处理使用`os.path`模块,支持Windows/Linux/macOS系统 3. **性能优化** 先收集全部路径再批量处理,相比实时遍历更高效(引用[5]的优化策略) ### 使用建议 1. **测试验证** 首次执行前建议创建备份: ```python import shutil shutil.copytree(r"E:\Pycharm_demo\yolov5\VOCData\image_1", r"E:\Pycharm_demo\yolov5\VOCData\image_1_backup") ``` 2. **异常处理** 若遇权限问题,可添加异常捕获: ```python except PermissionError as pe: print(f"权限错误:请关闭正在访问 {os.path.basename(old_path)} 的程序") ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值