Descripción general del bucle de ejecución

1. ¿Qué es Runloop?

Un bucle de ejecución consiste en hacer algo en un bucle mientras el programa se está ejecutando. Si no hay un bucle de ejecución, el programa saldrá después de la ejecución. Con runloop, el programa siempre puede ejecutarse y esperar la entrada del usuario. Runloop puede ejecutarse solo cuando sea necesario y dormir cuando no hay ninguna operación. Puede ahorrar recursos de la CPU y mejorar el rendimiento del programa.

2. Función de bucle de ejecución

1. Mantenga el programa ejecutándose continuamente. Se abrirá un subproceso principal tan pronto como se inicie el programa. Se creará un bucle de ejecución correspondiente tan pronto como se inicie el subproceso principal. El bucle de ejecución garantiza que el subproceso principal no se destruya, asegurando así la ejecución continua del programa.
2. Maneje varios eventos de aplicaciones, como eventos táctiles, eventos de temporizador, eventos de selector.
3. Ahorre recursos de CPU y mejore el rendimiento del programa. Cuando el hilo que abre el runloop no tiene operaciones que realizar, el hilo entrará en suspensión y liberará recursos de la CPU. Cuando haya algo que procesar, el hilo se activará inmediatamente para su procesamiento.
Principios internos de Runloop
La figura muestra que cuando Runloop se está ejecutando, cuando recibe fuentes de entrada (puerto mach, fuente de entrada personalizada, performSelector:onThread:..., etc.) o fuentes del temporizador, se entregará al método de procesamiento correspondiente para su procesamiento. Cuando no se reciben mensajes de eventos, el ciclo de ejecución descansa.

3. La relación entre RunLoop y los subprocesos.

Runloop se administra en función de pthread, que es una API subyacente de operación multiproceso multiplataforma basada en C. Es la encapsulación de capa superior del subproceso mach (consulte la Guía de programación del kernel) y se corresponde uno a uno con NSThread (NSThread es un conjunto de API orientadas a objetos, por lo que casi no necesitamos usar pthread directamente en el desarrollo de iOS ).

No existe una interfaz para crear Runloop directamente en la interfaz desarrollada por Apple. Si necesita usar Runloop, generalmente usa los métodos CFRunLoopGetMain () y CFRunLoopGetCurrent () de CF para obtenerlo. En consecuencia, puede llamar a [NSRunLoop currentRunLoop] en Foundation para obtener el objeto RunLoop del hilo actual y
llamar a [NSRunLoop mainRunLoop] para obtener el objeto RunLoop del hilo principal.

//获取主线程的RunLoop
CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    //传入主线程
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

//获取当前Runloop,没有就调用_CFRunLoopGet0
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

// 查看_CFRunLoopGet0方法内部
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
     //t为空默认是主线程
    if (pthread_equal(t, kNilPthreadT)) {
        t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    //__CFRunLoops字典为空
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    // 为主线程创建对应的RunLoop
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    // 将主线程-key和RunLoop-Value保存到字典中
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    
    // 将线程作为key从字典里获取一个loop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    
    // 如果loop为空,则创建一个新的loop,所以runloop会在第一次获取的时候创建
    if (!loop) {  
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    
    // 创建好之后,以线程为key,runloop为value,一对一存储在字典中,下次获取的时候,则直接返回字典内的runloop
    if (!loop) { 
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        loop = newLoop;
    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
    CFRelease(newLoop);
    }
    //如果传入的线程和方法调用的线程是同一个,且尚未为该runloop注册销毁函数,则为runloop注册一个销毁函数__CFFinalizeRunLoop
    if (pthread_equal(t, pthread_self())) {
     //将RunLoop存在线程特定数据区,提高后续查找速度   _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

Se puede ver en el código anterior:
1. Existe una correspondencia uno a uno entre los subprocesos y RunLoop. La relación se almacena en un Diccionario, con el subproceso como clave y RunLoop como valor.
2. Cuando queremos crear un RunLoop de un subproceso secundario, solo necesitamos obtener el objeto RunLoop del subproceso actual en el subproceso secundario, usando CFRunLoopGetCurrent() o [NSRunLoop currentRunLoop]. Cuando se llama al método, primero comprobará si hay un RunLoop en el diccionario que utiliza el subproceso. Si lo hay, devolverá directamente el RunLoop. Si no, creará uno y almacenará el subproceso correspondiente. -hilo en el diccionario. Si no se obtiene, el subproceso secundario no creará el RunLoop asociado con él.
3. El Runloop del hilo principal es especial: antes de crear cualquier hilo, se garantiza que el Runloop del hilo principal ya existe, y CFRunLoopGetMain () puede obtener el RunLoop del hilo principal independientemente de si se llama en el hilo principal o un hilo secundario.
4.RunLoop se crea cuando se adquiere por primera vez y se destruye cuando finaliza el hilo.

Además, cuando se inicia la aplicación, Runloop se inicia en la función UIApplicationMain en el hilo principal y el programa no saldrá inmediatamente, sino que permanecerá ejecutándose. Por lo tanto, cada aplicación debe tener un bucle de ejecución.

4. Explicación detallada de las clases y funciones relacionadas con RunLoop

Las clases relacionadas con RunLoop en el marco Foundation incluyen NSRunLoop. Las cinco clases sobre RunLoop en Core Foundation:

  1. CFRunLoopRef: obtiene el RunLoop actual y el RunLoop principal
  2. CFRunLoopModeRef: modo operativo RunLoop, solo puede elegir uno y realizar diferentes operaciones en diferentes modos
  3. CFRunLoopSourceRef: fuente de eventos, fuente de entrada
  4. CFRunLoopTimerRef - evento de temporizador
  5. CFRunLoopObserverRef - observador

4.1 CFRunLoopRef

4.1.1 Tipos de RunLoop

Ya sabemos antes que podemos obtener el RunLoop del hilo actual a través de la función CFRunLoopGetCurrent, y el RunLoop del hilo principal a través de la función CFRunLoopGetMain, así como algunas diferencias entre ellos.

4.1.2 Estructura de CFRunLoopRef

CFRunLoopRef es un puntero a la estructura __CFRunLoop.
A través del código fuente encontramos la estructura __CFRunLoop:

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;          /* locked for accessing mode list */
    __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread;
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

Además de algunos atributos de registro, echemos un vistazo principalmente a las siguientes dos variables miembro:
CFRunLoopModeRef _currentMode; que apunta al modo actual de RunLoop,
y CFMutableSetRef _modes; el modo incluido en RunLoop. CFMutableSetRef _commonModes contiene el nombre del modo marcado como modo común en _modes, y CFMutableSetRef _commonModeItems contiene el elemento de modo que debe agregarse al modo común. También lo discutiremos específicamente en CFRunLoopModeRef.

CFRunLoopModeRef representa el modo de ejecución de RunLoop.
Un RunLoop contiene varios modos, y cada modo contiene varias fuentes, temporizadores y observadores llamados elementos de modo.
Cuando RunLoop se inicia y se ejecuta, solo se puede y se debe especificar uno de los modos. Este modo se llama Modo actual.
Si necesita cambiar de Modo, solo puede salir del Modo actual y volver a especificar un Modo para ingresar, esto es principalmente para separar diferentes grupos de Fuente, Temporizador y Observador para que no se afecten entre sí. Si no hay Source0/Source1/Timer en modo, RunLoop saldrá inmediatamente.

Diagrama CFRunLoopModeRef

4.1.3 Operación de CFRunLoopRef

CFRunLoopRun
Ejecuta el objeto CFRunLoop del hilo actual en su modo predeterminado indefinidamente.Deja
que RunLoop del hilo actual se ejecute en el modo predeterminado.

CFRunLoopRunInMode
Ejecuta el objeto CFRunLoop del hilo actual en un modo particular.Deja
que RunLoop del hilo actual se ejecute en el modo especificado.

CFRunLoopWakeUp
Despierta un objeto CFRunLoop en espera
Despierta un RunLoop inactivo.

CFRunLoopStop
Fuerza que un objeto CFRunLoop deje de ejecutarse.
Detiene la ejecución del RunLoop.

CFRunLoopIsWaiting
Devuelve un valor booleano que indica si el bucle de ejecución está esperando un evento.
Determina si un RunLoop está inactivo y esperando que el evento se active.

4.2 CFRunLoopModeRef

4.2.1 tipo de modo

Hay 5 modos registrados de forma predeterminada en el sistema en iOS. Hay dos modos proporcionados públicamente por Apple: kCFRunLoopDefaultMode (NSDefaultRunLoopMode) y UITrackingRunLoopMode. Puede usar estos dos nombres de modo para operar sus modos correspondientes.

Los cinco modos de funcionamiento son:

  1. kCFRunLoopDefaultMode: modo predeterminado de la aplicación. Normalmente, el hilo principal se ejecuta en este modo.
  2. UITrackingRunLoopMode: modo de seguimiento de interfaz, utilizado para que ScrollView rastree el deslizamiento táctil para garantizar que la interfaz no se vea afectada por otros modos al deslizarse.
  3. UIInitializationRunLoopMode: el primer modo ingresado cuando se inicia la aplicación por primera vez. Ya no se utilizará una vez que se complete el inicio.
  4. GSEventReceiveRunLoopMode: modo interno para recibir eventos del sistema, generalmente no utilizado
  5. kCFRunLoopCommonModes: este es un modo de marcador de posición, utilizado como marca para kCFRunLoopDefaultMode y UITrackingRunLoopMode, y no es un modo real.

Existe un concepto llamado "CommonModes": un modo puede marcarse a sí mismo como un atributo "común" (agregando su nombre de modo a los "commonModes" de RunLoop). Siempre que cambie el contenido de RunLoop, RunLoop sincronizará automáticamente la Fuente/Observador/Temporizador en _commonModeItems con todos los Modos con la etiqueta "Común".

Ejemplo de escenario de aplicación: hay dos modos preestablecidos en RunLoop del hilo principal: kCFRunLoopDefaultMode y UITrackingRunLoopMode. Ambos modos se han marcado como atributos "comunes". DefaultMode es el estado normal de la aplicación y TrackingRunLoopMode es el estado en el que se realiza el seguimiento del deslizamiento de ScrollView. Cuando crea un temporizador y lo agrega a DefaultMode, el temporizador recibirá repetidas devoluciones de llamada, pero cuando desliza un TableView, RunLoop cambiará el modo a TrackingRunLoopMode. En este momento, no se devolverá la llamada al temporizador y no Afecta la operación de deslizamiento.

A veces necesitas un temporizador que pueda recibir devoluciones de llamada en ambos modos. Una forma es agregar este temporizador a los dos modos respectivamente. Otra forma es agregar el temporizador a los "commonModeItems" del RunLoop de nivel superior. RunLoop actualiza automáticamente "commonModeItems" a todos los modos con el atributo "Común".

4.2.2 Estructura de CFRunLoopModeRef

CFRunLoopModeRef es en realidad un puntero a la estructura __CFRunLoopMode. El código fuente de la estructura __CFRunLoopMode es el siguiente:

typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};

Ver principalmente las siguientes variables miembro

CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;

En CFRunLoopModeRef, ya analizamos la relación entre CFRunLoopModeRef, CFRunLoopModeRef y el elemento de modo, por lo que no entraremos en detalles aquí.

El elemento de modo incluye Fuente1/Fuente0/Temporizadores/Observador. ¿Qué representan?

  1. Fuente1: comunicación entre subprocesos basada en puertos
  2. Fuente0: eventos que no son de puerto, incluidos eventos táctiles, PerformSelectores relacionados con subprocesos sin demora, etc.
  3. Temporizadores: temporizadores, NSTimer
  4. Observador: oyente, utilizado para monitorear el estado de RunLoop

4.2.3 Operación del modo RunLoop

En Core Foundation, Apple solo abre las siguientes tres API para operaciones de modo (Cocoa también tiene funciones con la misma función, que no aparecerán nuevamente en la lista):

CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode)
Adds a mode to the set of run loop common modes.
向当前RunLoop的common modes中添加一个mode。

CFStringRef CFRunLoopCopyCurrentMode(CFRunLoopRef rl)
CFArray
返回当前运行的mode的name

Ref CFRunLoopCopyAllModes(CFRunLoopRef rl)
返回当前RunLoop的所有mode

CFRunLoopAddCommonMode

No tenemos forma de crear directamente un objeto CFRunLoopMode, pero podemos llamar a CFRunLoopAddCommonMode para pasar una cadena para agregar Modo a RunLoop. La cadena pasada es el nombre de Modo. Cuando no hay un modo correspondiente dentro de RunLoop, RunLoop creará automáticamente un el correspondiente para usted: CFRunLoopModeRef. Para un RunLoop, su modo interno solo se puede agregar pero no eliminar. Echemos un vistazo al código fuente.

void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    __CFRunLoopLock(rl);
    //看rl中是否已经有这个mode,如果有就什么都不做
    if (!CFSetContainsValue(rl->_commonModes, modeName)) {
        CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;
        //把modeName添加到RunLoop的_commonModes中
        CFSetAddValue(rl->_commonModes, modeName);
        if (NULL != set) {
            CFTypeRef context[2] = {rl, modeName};
            /* add all common-modes items to new mode */
            //这里调用CFRunLoopAddSource/CFRunLoopAddObserver/CFRunLoopAddTimer的时候会调用
            //__CFRunLoopFindMode(rl, modeName, true),CFRunLoopMode对象在这个时候被创建
            CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
            CFRelease(set);
        }
    } else {
    }
    __CFRunLoopUnlock(rl);
}

Puede observarse que:

1.modeName no se puede repetir.modeName es el identificador único del modo.
2.La matriz _commonModes de RunLoop almacena los nombres de todos los modos marcados como comunes.
3. Agregar commonMode sincronizará todas las fuentes en la matriz commonModeItems con el modo recién agregado.
4. El objeto CFRunLoopMode se crea cuando la función CFRunLoopAddItemsToCommonMode llama a CFRunLoopFindMode

CFRunLoopCopyCurrentMode/CFRunLoopCopyAllModes

La lógica interna de CFRunLoopCopyCurrentMode y CFRunLoopCopyAllModes es relativamente simple. Simplemente tome _currentMode y _modes de RunLoop y devuélvalos. El código fuente no se publicará.

4.2.4 Cambiar entre modos

Programe un temporizador en el hilo principal y ejecútelo normalmente, y luego, al deslizar TableView o UIScrollView en la interfaz, el temporizador anterior no se activa. ¿por qué?

[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTimer1) userInfo:nil repeats:YES];

Este problema también está relacionado con RunLoopMode de RunLoop. Porque al programar un temporizador, el temporizador se agrega al NSDefaultRunLoopMode de RunLoop, y al deslizar TableView o UIScrollView, el modo RunLoop se cambia a UITrackingRunLoopMode. RunLoop solo rastreará los eventos deslizantes en la interfaz de usuario y el temporizador no será suspendido será activado.

La función de cambio de modo no está implementada en el marco CF, por lo que UIKit debe manejarla por sí mismo cuando la use. El proceso general es que UIApplication activa un modo que está a punto de ejecutarse y luego activa RunLoop. Al volver a cambiar, solo necesita activar este modo y luego activarlo.
[La transferencia de la imagen del enlace externo falló. El sitio de origen puede tener un mecanismo anti-leeching. Se recomienda guardar la imagen y cargarla directamente (img-FlolCpqS-1652108715759)(https://upload-images.jianshu.io/ upload_images/1049809-74da0b6560623ec5.png ?imageMogr2/auto-orient/strip%7CimageView2/2/w/1000)]

4.3 Fuente del evento CFRunLoopSourceRef

4.3.1 Tipos de CFRunLoopSource

CFRunLoopSource
Puede ver estas dos cosas CFMutableSetRef _source0 y CFMutableSetRef _source1 en mi código de estructura de datos RunLoopMode. En primer lugar, estas dos cosas son Set (conjunto), y el conjunto almacena un montón de estructuras de datos. Entonces, ¿cuál es esta fuente? , En realidad, también son una estructura de datos CFRunLoopSourceRef.

CFRunLoopSource es una abstracción de fuentes de entrada. CFRunLoopSource se divide en dos versiones: source0 y source1.

Fuente0: no basado en el puerto, se usa para eventos activados activamente por el usuario (al hacer clic en un botón o en la pantalla, los PerformSelectors relacionados con el subproceso sin demora son la fuente0 y el que tiene demora es el temporizador).
Fuente1: basado en puerto, se envían mensajes entre sí a través del kernel y otros subprocesos (relacionados con el kernel).

4.3.2 Estructura de CFRunLoopSourceRef

CFRunLoopSourceRef es un puntero a la estructura __CFRunLoopSource, su estructura es la siguiente:

struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits;   //用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被处理
    pthread_mutex_t _lock;
    CFIndex _order;            /* immutable */
    CFMutableBagRef _runLoops;
    union {
      CFRunLoopSourceContext version0;    /* source0的数据结构 */
        CFRunLoopSourceContext1 version1;    /* source1的数据结构 */
    } _context;
};

La estructura de datos __CFRunLoopSource contiene un miembro _context, su tipo es CFRunLoopSourceContext o CFRunLoopSourceContext1, que especifica el tipo de fuente.

source0
source0 es un evento interno de la App. UIEvent y CFSocket administrados por la propia App son ambos source0. Cuando un evento de Source0 está listo para ser ejecutado, primero debe marcarse como señal. La siguiente es la estructura de Source0:

//source0
typedef struct {
    CFIndex    version;  // 版本号,用来区分是source1还是source0
    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);
    void    (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
    void    (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
    void    (*perform)(void *info);
} CFRunLoopSourceContext;

source0 no está basado en puerto. Solo contiene una devolución de llamada (puntero de función), que no puede activar eventos de forma activa. Al usarlo, primero debe llamar a CFRunLoopSourceSignal (fuente) para marcar la Fuente como pendiente y luego llamar manualmente a CFRunLoopWakeUp (runloop) para activar RunLoop y dejar que maneje el evento.

source1
source1 es administrado por RunLoop y el kernel. Source1 tiene mach_port_t, que puede recibir mensajes del kernel y activar devoluciones de llamadas. La siguiente es la estructura de source1

//source1
typedef struct {
    CFIndex    version;  // 版本号,用来区分是source1还是source0
    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);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
    mach_port_t    (*getPort)(void *info);  // 端口
    void *    (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
    void *    (*getPort)(void *info);
    void    (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;

Además del puntero de devolución de llamada, Source1 contiene un puerto mach. Source1 puede escuchar el puerto del sistema, comunicarse con otros subprocesos a través del kernel, recibir y distribuir eventos del sistema. Puede activar activamente RunLoop (administrado por el kernel del sistema operativo, como como mensajes CFMessagePort). El funcionario también señaló que la fuente se puede personalizar, por lo que es más como un protocolo para CFRunLoopSourceRef. El marco ha definido dos implementaciones de forma predeterminada. Los desarrolladores también pueden personalizarlo si es necesario. Para obtener más detalles, puede consultar la documentación oficial .

4.3.3 Operación de CFRunLoopSource

CFRunLoopAddSource
La estructura del código de CFRunLoopAddSource es la siguiente:

//添加source事件
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) {    /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    if (!__CFIsValid(rls)) return;
    Boolean doVer0Callout = false;
    __CFRunLoopLock(rl);
    //如果是kCFRunLoopCommonModes
    if (modeName == kCFRunLoopCommonModes) {
        //如果runloop的_commonModes存在,则copy一个新的复制给set
        CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
       //如果runl _commonModeItems为空
        if (NULL == rl->_commonModeItems) {
            //先初始化
            rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
        }
        //把传入的CFRunLoopSourceRef加入_commonModeItems
        CFSetAddValue(rl->_commonModeItems, rls);
        //如果刚才set copy到的数组里有数据
        if (NULL != set) {
            CFTypeRef context[2] = {rl, rls};
            /* add new item to all common-modes */
            //则把set里的所有mode都执行一遍__CFRunLoopAddItemToCommonModes函数
            CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
            CFRelease(set);
        }
        //以上分支的逻辑就是,如果你往kCFRunLoopCommonModes里面添加一个source,那么所有_commonModes里的mode都会添加这个source
    } else {
        //根据modeName查找mode
        CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
        //如果_sources0不存在,则初始化_sources0,_sources0和_portToV1SourceMap
        if (NULL != rlm && NULL == rlm->_sources0) {
            rlm->_sources0 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
            rlm->_sources1 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
            rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, NULL);
        }
        //如果_sources0和_sources1中都不包含传入的source
        if (NULL != rlm && !CFSetContainsValue(rlm->_sources0, rls) && !CFSetContainsValue(rlm->_sources1, rls)) {
            //如果version是0,则加到_sources0
            if (0 == rls->_context.version0.version) {
                CFSetAddValue(rlm->_sources0, rls);
                //如果version是1,则加到_sources1
            } else if (1 == rls->_context.version0.version) {
                CFSetAddValue(rlm->_sources1, rls);
                __CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
                if (CFPORT_NULL != src_port) {
                    //此处只有在加到source1的时候才会把souce和一个mach_port_t对应起来
                    //可以理解为,source1可以通过内核向其端口发送消息来主动唤醒runloop
                    CFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls);
                    __CFPortSetInsert(src_port, rlm->_portSet);
                }
            }
            __CFRunLoopSourceLock(rls);
            //把runloop加入到source的_runLoops中
            if (NULL == rls->_runLoops) {
                rls->_runLoops = CFBagCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeBagCallBacks); // sources retain run loops!
            }
            CFBagAddValue(rls->_runLoops, rl);
            __CFRunLoopSourceUnlock(rls);
            if (0 == rls->_context.version0.version) {
                if (NULL != rls->_context.version0.schedule) {
                    doVer0Callout = true;
                }
            }
        }
        if (NULL != rlm) {
            __CFRunLoopModeUnlock(rlm);
        }
    }
    __CFRunLoopUnlock(rl);
    if (doVer0Callout) {
        // although it looses some protection for the source, we have no choice but
        // to do this after unlocking the run loop and mode locks, to avoid deadlocks
        // where the source wants to take a lock which is already held in another
        // thread which is itself waiting for a run loop/mode lock
        rls->_context.version0.schedule(rls->_context.version0.info, rl, modeName); /* CALLOUT */
    }
}

Al agregar este código fuente, podemos sacar las siguientes conclusiones:

Si modeName se pasa a kCFRunLoopCommonModes, la fuente se guardará en _commonModeItems de RunLoop y se agregará a todos los elementos del modo Modo común.
Si el nombre del modo pasado no es kCFRunLoopCommonModes, el modo se buscará primero. Si no, se creará uno. La
misma fuente solo se puede agregar una vez en un modo.

CFRunLoopRemoveSource

La lógica de la operación de eliminación y la operación de adición son básicamente la misma y fáciles de entender.

//移除source
void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */
    CHECK_FOR_FORK();
    Boolean doVer0Callout = false, doRLSRelease = false;
    __CFRunLoopLock(rl);
    //如果是kCFRunLoopCommonModes,则从_commonModes的所有mode中移除该source
    if (modeName == kCFRunLoopCommonModes) {
        if (NULL != rl->_commonModeItems && CFSetContainsValue(rl->_commonModeItems, rls)) {
            CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
            CFSetRemoveValue(rl->_commonModeItems, rls);
            if (NULL != set) {
                CFTypeRef context[2] = {rl, rls};
                /* remove new item from all common-modes */
                CFSetApplyFunction(set, (__CFRunLoopRemoveItemFromCommonModes), (void *)context);
                CFRelease(set);
            }
        } else {
        }
    } else {
        //根据modeName查找mode,如果不存在,返回NULL
        CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, false);
        if (NULL != rlm && ((NULL != rlm->_sources0 && CFSetContainsValue(rlm->_sources0, rls)) || (NULL != rlm->_sources1 && CFSetContainsValue(rlm->_sources1, rls)))) {
            CFRetain(rls);
            //根据source版本做对应的remove操作
            if (1 == rls->_context.version0.version) {
                __CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
                if (CFPORT_NULL != src_port) {
                    CFDictionaryRemoveValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port);
                    __CFPortSetRemove(src_port, rlm->_portSet);
                }
            }
            CFSetRemoveValue(rlm->_sources0, rls);
            CFSetRemoveValue(rlm->_sources1, rls);
            __CFRunLoopSourceLock(rls);
            if (NULL != rls->_runLoops) {
                CFBagRemoveValue(rls->_runLoops, rl);
            }
            __CFRunLoopSourceUnlock(rls);
            if (0 == rls->_context.version0.version) {
                if (NULL != rls->_context.version0.cancel) {
                    doVer0Callout = true;
                }
            }
            doRLSRelease = true;
        }
        if (NULL != rlm) {
            __CFRunLoopModeUnlock(rlm);
        }
    }
    __CFRunLoopUnlock(rl);
    if (doVer0Callout) {
        // although it looses some protection for the source, we have no choice but
        // to do this after unlocking the run loop and mode locks, to avoid deadlocks
        // where the source wants to take a lock which is already held in another
        // thread which is itself waiting for a run loop/mode lock
        rls->_context.version0.cancel(rls->_context.version0.info, rl, modeName);   /* CALLOUT */
    }
    if (doRLSRelease) CFRelease(rls);
}

CFRunLoopContainsSource
booleano CFRunLoopContainsSource (CFRunLoopRef rl, fuente CFRunLoopSourceRef, modo CFRunLoopMode);
determine si la fuente especificada existe en el modo especificado en RunLoop

Aquí hay un breve resumen:
1. CFRunLoopSourceRef es donde ocurre el evento;
2. Este CFRunLoopSourceRef tiene dos versiones, fuente0 y fuente1;
3. fuente0 solo contiene una devolución de llamada (puntero de función) y no puede iniciar eventos activamente. Se requiere CFRunLoopSourceSignal(fuente). El origen está marcado como pendiente, CFRunLoopWakeUp(runloop) activa RunLoop y le permite procesar el evento
4. El origen1 contiene mach_port y una devolución de llamada (puntero de función), que se utiliza para enviarse mensajes entre sí a través del núcleo y otros subprocesos, y puede despierta activamente RunLoop.
5. Las fuentes de entrada distribuyen eventos asincrónicos a los controladores correspondientes, lo que hace que runUntilDate: salga, mientras que los temporizadores distribuyen eventos sincrónicos a sus controladores, lo que no hará que RunLoop salga.
6.El selector de ejecución es una fuente de entrada personalizada. Las solicitudes de selección de ejecución se reciben en serie en el hilo de destino. Después de ejecutar el selector, se eliminará del runloop, pero la fuente basada en puerto no. runloop procesará todos los selectores cada vez que se ejecute, en lugar de procesar uno a la vez.

4.4 CFRunLoopTimerRef

CFRunLoopTimerRef es un activador basado en el tiempo. Este y NSTimer están conectados de forma gratuita y se pueden combinar. Por lo tanto, NSTimer es una encapsulación de RunLoopTimer. CFRunLoopTimerRef contiene un período de tiempo y una devolución de llamada (puntero de función). Cuando se agrega a RunLoop, RunLoop registrará el punto de tiempo correspondiente. Cuando llegue el punto de tiempo, RunLoop se despertará para ejecutar esa devolución de llamada.

4.4.1 Estructura de CFRunLoopTimerRef

CFRunLoopTimerRef es un puntero a la estructura __CFRunLoopTimer.

struct __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 */
};

4.4.2 Operación de CFRunLoopTimerRef

void CFRunLoopAddTimer (CFRunLoopRef rl, temporizador CFRunLoopTimerRef, modo CFStringRef)
agrega el temporizador especificado al modo especificado del RunLoop especificado

void CFRunLoopRemoveTimer(CFRunLoopRef rl, temporizador CFRunLoopTimerRef, modo CFStringRef)
elimina el temporizador especificado en el modo especificado del RunLoop especificado

Booleano CFRunLoopContainsTimer (CFRunLoopRef rl, temporizador CFRunLoopTimerRef, modo CFRunLoopMode);
determina si el temporizador especificado existe en el modo especificado del RunLoop especificado

CFAbsoluteTime CFRunLoopGetNextTimerFireDate (CFRunLoopRef rl, modo CFRunLoopMode);
obtiene la última hora de activación de todos los temporizadores registrados en el modo especificado del RunLoop especificado

####4.4.3 Implementación del temporizador
Por razones de espacio, se presentará en otro artículo.

4.5CFRunLoopObserverRef

4.5.1 Estado de ejecución del bucle

CFRunLoopObserverRef es un oyente en el bucle de mensajes que puede monitorear los cambios de estado de RunLoop y notificar al exterior sobre el estado de ejecución actual de RunLoop en cualquier momento (contiene un puntero de función _callout_ para informarle al observador el estado actual a tiempo).

El estado de RunLoop (CFOptionFlags) incluye lo siguiente:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),               // 即将进入Loop
    kCFRunLoopBeforeTimers = (1UL << 1),        // runLoop即将处理 Timers
    kCFRunLoopBeforeSources = (1UL << 2),       // runLoop即将处理 Sources
    kCFRunLoopBeforeWaiting = (1UL << 5),       // runLoop即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),        // runLoop刚从休眠中唤醒
    kCFRunLoopExit = (1UL << 7),                // 即将退出RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU       
};

4.5.2 Estructura de CFRunLoopObserverRef

La estructura de CFRunLoopObserverRef es la siguiente:

struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;    //observer对应的runLoop
    CFIndex _rlCount;              //observer当前监测的runLoop数量
    CFOptionFlags _activities;   //observer观测runLoop的状态,枚举类型
    CFIndex _order;            //CFRunLoopMode中是数组形式存储的observer,_order就是他在数组中的位置
    CFRunLoopObserverCallBack _callout;    //observer观察者的函数回调
    CFRunLoopObserverContext _context;    /* immutable, except invalidation */
};

Aquí hay un campo _order, que determina la posición del observador: cuanto menor es el valor, mayor es y se puede llamar antes. De manera similar, __CFRunLoopSource, __CFRunLoopTimer y __CFRunLoopObserver tienen dichos campos. El uso de _order también se presentará en detalle en 7.1 AutoreleasePool.

Aquí debemos prestar atención al campo _callout. Durante el proceso de desarrollo, casi todas las operaciones se devuelven a través de Llamada (ya sea notificación de estado de Observador o procesamiento de Temporizador y Fuente), y el sistema generalmente usa las siguientes funciones para realizar devoluciones de llamada (en otras palabras, su El código es en última instancia Se llama a través de las siguientes funciones. Incluso si escucha al observador usted mismo, primero llamará a las siguientes funciones y luego le notificará indirectamente, por lo que a menudo verá estas funciones en la pila de llamadas):

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
    static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

Por ejemplo, __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(rlo->_callout, rlo, actividad, rlo->_context.info) llamará internamente a la función señalada por rlo->_callout.

Establecimos un punto de interrupción en touchBegin del controlador para ver la pila (dado que UIEvent es Source0, puede ver una función de llamada CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION de Source0):

4.5.3 Operación de CFRunLoopObserverRef

void CFRunLoopAddObserver (CFRunLoopRef rl, observador CFRunLoopObserverRef, modo CFStringRef)
agrega el observador especificado a RunLoop en el modo especificado

void CFRunLoopRemoveObserver (CFRunLoopRef rl, observador CFRunLoopObserverRef, modo CFStringRef *)
elimina el observador especificado en el modo especificado RunLoop

Booleano CFRunLoopContainsObserver (CFRunLoopRef rl, observador CFRunLoopObserverRef, modo CFRunLoopMode);
determina si hay un observador específico en el modo RunLoop especificado

Discusión:
La lógica interna de agregar un observador y un temporizador es más o menos similar a agregar una fuente.
La diferencia es que el observador y el temporizador solo se pueden agregar a uno o más modos de un RunLoop. Por ejemplo, si se agrega un temporizador al RunLoop del subproceso principal, el temporizador no se puede agregar al RunLoop del subproceso secundario. No tiene esta restricción. No importa qué RunLoop sea, siempre que no esté en el modo, se puede agregar.
Esta diferencia también se puede encontrar en algunas de las estructuras anteriores: la estructura CFRunLoopSource tiene una matriz que guarda los objetos RunLoop, mientras que CFRunLoopObserver y CFRunLoopTimer solo tienen un único objeto RunLoop.

5. Inicio y salida de RunLoop

5.1Inicio de RunLoop

Inicio de RunLoop en Foundation
Existen las siguientes tres formas de iniciar un runloop en Foundation. No importa cuál de estos tres métodos se utilice para iniciar el runloop, si no hay una fuente de entrada o un temporizador adjunto al runloop, el runloop saldrá inmediatamente .

  • (nulo)correr;
  • (void)runUntilDate:(NSDate *)limitDate;
  • (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

(1) En el primer método, el runloop continuará ejecutándose, durante el cual se procesarán los datos de la fuente de entrada, y el método runMode:beforeDate: se llamará repetidamente en el modo NSDefaultRunLoopMode; (2) En el segundo método , se puede
establecer un tiempo de espera, antes de que se alcance el tiempo de espera, el runloop seguirá ejecutándose. Durante este período, el runloop procesará los datos de la fuente de entrada y también llamará repetidamente al método runMode:beforeDate: en NSDefaultRunLoopMode. modo; (3) La tercera forma, el
runloop Una vez ejecutado, la fuente de entrada lo bloquea en el modo especificado. Cuando se agota el tiempo de espera o se procesa la primera fuente de entrada, esta ejecución saldrá.

Los dos primeros métodos de inicio llamarán repetidamente al método runMode:beforeDate:.

Inicio de RunLoop en CF

Veamos el código fuente para iniciar RunLoop en CF:

 // 用DefaultMode启动
void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

//指定mode的一次运行
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

Descubrimos que RunLoop se implementa mediante do while al juzgar el valor del resultado.

5.2 Salida de RunLoop

1. El hilo principal se destruye y RunLoop sale. (El hilo se destruye y sale)
2. Hay algunos temporizadores y fuentes en el modo, que garantizan que cuando el modo no esté vacío, RunLoop no esté inactivo y se esté ejecutando. Cuando el modo esté vacío, RunLoop saldrá inmediatamente. (Pero Apple no recomienda que hagamos esto, porque el sistema puede agregar algunas fuentes de entrada al ciclo de ejecución del subproceso actual, por lo que eliminar manualmente la fuente de entrada o el temporizador no garantiza que el ciclo de ejecución definitivamente saldrá). item)
3. Podemos establecer cuándo detenernos al iniciar RunLoop. (salida del tiempo de espera)

Cómo salir de NSRunLoop

6. Lógica de procesamiento RunLoop

6.1 Procesamiento de eventos RunLoop

Ya hemos introducido el inicio de RunLoop en CF ¿Cómo se ejecuta RunLoop? Esto está relacionado con la función CFRunLoopRunSpecific.

La función CFRunLoopRunSpecific se llama en la función CFRunLoopRun, el parámetro runloop se pasa al objeto RunLoop actual y el parámetro modeName se pasa a kCFRunLoopDefaultMode.

La función CFRunLoopRunSpecific también se llama en la función CFRunLoopRunInMode. El parámetro runloop se pasa en el objeto RunLoop actual y el parámetro modeName continúa pasando el modeName pasado por CFRunLoopRunInMode.

Echemos un vistazo al código fuente de CFRunLoopRunSpecific.

/*
 * 指定mode运行runloop
 * @param rl 当前运行的runloop
 * @param modeName 需要运行的mode的name
 * @param seconds  runloop的超时时间
 * @param returnAfterSourceHandled 是否处理完事件就返回
 */
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    __CFRunLoopLock(rl);
    //根据modeName找到本次运行的mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    //如果没找到 || mode中没有注册任何事件,则就此停止,不进入循环
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        Boolean did = false;
        if (currentMode) __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopUnlock(rl);
        return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    //保存上一次运行的mode
    CFRunLoopModeRef previousMode = rl->_currentMode;
    //更新本次mode
    rl->_currentMode = currentMode;
    //初始化一个result为kCFRunLoopRunFinished
    int32_t result = kCFRunLoopRunFinished;
    
    // 1.通知observer即将进入runloop
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //10.通知observer已退出runloop
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    __CFRunLoopModeUnlock(currentMode);
    __CFRunLoopPopPerRunData(rl, previousPerRun);
    rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);
    return result;
}

A través de la lógica interna de CFRunLoopRunSpecific, podemos concluir:

  1. Si especifica un modo inexistente para ejecutar RunLoop, fallará y el modo no se creará, por lo que el modo pasado aquí debe existir.
  2. Si se especifica un modo, pero el modo no contiene ningún modeItem, RunLoop no se ejecutará, por lo que se debe pasar un modo que contenga al menos un modeItem.
  3. Notifique al observador antes de ingresar al ciclo de ejecución, el estado es kCFRunLoopEntry
  4. Notifique al observador después de salir del ciclo de ejecución, el estado es kCFRunLoopExit

La función principal de la operación RunLoop es __CFRunLoopRun. A continuación, analizamos el código fuente de __CFRunLoopRun.
__CFRunLoopEjecutar:

/**
 *  运行run loop
 *
 *  @param rl              运行的RunLoop对象
 *  @param rlm             运行的mode
 *  @param seconds         run loop超时时间
 *  @param stopAfterHandle true:run loop处理完事件就退出  false:一直运行直到超时或者被手动终止
 *  @param previousMode    上一次运行的mode
 *
 *  @return 返回4种状态
 */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    //获取系统启动后的CPU运行时间,用于控制超时时间
    uint64_t startTSR = mach_absolute_time();
    
    //如果RunLoop或者mode是stop状态,则直接return,不进入循环
    if (__CFRunLoopIsStopped(rl)) {
        __CFRunLoopUnsetStopped(rl);
        return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
        rlm->_stopped = false;
        return kCFRunLoopRunStopped;
    }
    
    //mach端口,在内核中,消息在端口之间传递。 初始为0
    mach_port_name_t dispatchPort = MACH_PORT_NULL;
    //判断是否为主线程
    Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
    //如果在主线程 && runloop是主线程的runloop && 该mode是commonMode,则给mach端口赋值为主线程收发消息的端口
    if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();
    
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    mach_port_name_t modeQueuePort = MACH_PORT_NULL;
    if (rlm->_queue) {
        //mode赋值为dispatch端口_dispatch_runloop_root_queue_perform_4CF
        modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
        if (!modeQueuePort) {
            CRASH("Unable to get port for run loop mode queue (%d)", -1);
        }
    }
#endif
    
    //GCD管理的定时器,用于实现runloop超时机制
    dispatch_source_t timeout_timer = NULL;
    struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
    
    //立即超时
    if (seconds <= 0.0) { // instant timeout
        seconds = 0.0;
        timeout_context->termTSR = 0ULL;
    }
    //seconds为超时时间,超时时执行__CFRunLoopTimeout函数
    else if (seconds <= TIMER_INTERVAL_LIMIT) {
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, DISPATCH_QUEUE_OVERCOMMIT);
        timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        dispatch_retain(timeout_timer);
        timeout_context->ds = timeout_timer;
        timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
        timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
        dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
        dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
        dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
        uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
        dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
        dispatch_resume(timeout_timer);
    }
    //永不超时
    else { // infinite timeout
        seconds = 9999999999.0;
        timeout_context->termTSR = UINT64_MAX;
    }
    
    //标志位默认为true
    Boolean didDispatchPortLastTime = true;
    //记录最后runloop状态,用于return
    int32_t retVal = 0;
    do {
        //初始化一个存放内核消息的缓冲池
        uint8_t msg_buffer[3 * 1024];
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        mach_msg_header_t *msg = NULL;
        mach_port_t livePort = MACH_PORT_NULL;
#endif
        //取所有需要监听的port
        __CFPortSet waitSet = rlm->_portSet;
        
        //设置RunLoop为可以被唤醒状态
        __CFRunLoopUnsetIgnoreWakeUps(rl);
        
        //2.通知observer,即将触发timer回调,处理timer事件
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        //3.通知observer,即将触发Source0回调
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        
        //执行加入当前runloop的block
        __CFRunLoopDoBlocks(rl, rlm);
        
        //4.处理source0事件
        //有事件处理返回true,没有事件返回false
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            //执行加入当前runloop的block
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        //如果没有Sources0事件处理 并且 没有超时,poll为false
        //如果有Sources0事件处理 或者 超时,poll都为true
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        
        //第一次do..whil循环不会走该分支,因为didDispatchPortLastTime初始化是true
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
            //从缓冲区读取消息
            msg = (mach_msg_header_t *)msg_buffer;
            //5.接收dispatchPort端口的消息,(接收source1事件)
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0)) {
                //如果接收到了消息的话,前往第9步开始处理msg
                goto handle_msg;
            }
#endif
        }
        
        didDispatchPortLastTime = false;
        
        //6.通知观察者RunLoop即将进入休眠
        if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        //设置RunLoop为休眠状态
        __CFRunLoopSetSleeping(rl);
        // do not do any user callouts after this point (after notifying of sleeping)
        
        // Must push the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced.
        
        __CFPortSetInsert(dispatchPort, waitSet);
        
        __CFRunLoopModeUnlock(rlm);
        __CFRunLoopUnlock(rl);
        
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        //这里有个内循环,用于接收等待端口的消息
        //进入此循环后,线程进入休眠,直到收到新消息才跳出该循环,继续执行run loop
        do {
            if (kCFUseCollectableAllocator) {
                objc_clear_stack(0);
                memset(msg_buffer, 0, sizeof(msg_buffer));
            }
            msg = (mach_msg_header_t *)msg_buffer;
            //7.__CFRunLoopServiceMachPort调用mach_msg等待接受mach_port的消息。线程将进入休眠,知道被下面某个事件唤醒:一个基于Port的Source事件,也就是Source1;一个Timer到时间了;RunLoop自身超时时间到了;被其他什么调用者手动唤醒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
            //收到消息之后,livePort的值为msg->msgh_local_port,
            if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
                // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
                while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
                if (rlm->_timerFired) {
                    // Leave livePort as the queue port, and service timers below
                    rlm->_timerFired = false;
                    break;
                } else {
                    if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
                }
            } else {
                // Go ahead and leave the inner loop.
                break;
            }
        } while (1);
#else
        if (kCFUseCollectableAllocator) {
            objc_clear_stack(0);
            memset(msg_buffer, 0, sizeof(msg_buffer));
        }
        msg = (mach_msg_header_t *)msg_buffer;
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
#endif      
#endif
        
        __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);
        
        // Must remove the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced. Also, we don't want them left
        // in there if this function returns.
        
        __CFPortSetRemove(dispatchPort, waitSet);
    __CFRunLoopSetIgnoreWakeUps(rl);
        
        // user callouts now OK again
        //取消runloop的休眠状态
        __CFRunLoopUnsetSleeping(rl);
        //8.通知观察者runloop被唤醒
        if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
      
        //9.处理收到的消息
    handle_msg:;
        __CFRunLoopSetIgnoreWakeUps(rl);
        
        if (MACH_PORT_NULL == livePort) {
            CFRUNLOOP_WAKEUP_FOR_NOTHING();
            // handle nothing
            //通过CFRunloopWake唤醒
        } else if (livePort == rl->_wakeUpPort) {
            CFRUNLOOP_WAKEUP_FOR_WAKEUP();
            //什么都不干,跳回2重新循环
            // do nothing on Mac OS
       }
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        //如果是定时器事件
        else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            //9.1 处理timer事件
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer, because we apparently fired early
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
#if USE_MK_TIMER_TOO
        //如果是定时器事件
        else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            // On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
            // In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754
           //9.1处理timer事件
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
        //如果是dispatch到main queue的block
        else if (livePort == dispatchPort) {
            CFRUNLOOP_WAKEUP_FOR_DISPATCH();
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
           //9.2执行block
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            sourceHandledThisLoop = true;
            didDispatchPortLastTime = true;
        } else {
            CFRUNLOOP_WAKEUP_FOR_SOURCE();
            // Despite the name, this works for windows handles as well
            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
            // 有source1事件待处理
            if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
                mach_msg_header_t *reply = NULL;
                //9.2 处理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);
                }
#endif
            }
        }
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
#endif
        
        __CFRunLoopDoBlocks(rl, rlm);
        //判断是否要退出 RunLoop
        if (sourceHandledThisLoop && stopAfterHandle) {
            //进入run loop时传入的参数,处理完事件就返回
            retVal = kCFRunLoopRunHandledSource;
        }else if (timeout_context->termTSR < mach_absolute_time()) {
            //run loop超时
            retVal = kCFRunLoopRunTimedOut;
        }else if (__CFRunLoopIsStopped(rl)) {
            //run loop被手动终止
            __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        }else if (rlm->_stopped) {
            //调用_CFRunLoopStopMode使mode被终止
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        }else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            //mode中没有要处理的事件
            retVal = kCFRunLoopRunFinished;
        }
        //除了上面这几种情况,都继续循环
    } while (0 == retVal);
    
    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {
        free(timeout_context);
    }
    
    return retVal;
}

Una cosa a tener en cuenta aquí es que la macro USE_DISPATCH_SOURCE_FOR_TIMERS es 1 en Mac y 0 en iOS, por lo que parte del código aquí no existe en iOS.
En iOS, __CFRunLoopRun tiene solo un ciclo do- while, que puede considerarse como el ciclo de vida completo de RunLoop. Cuando se cumplen algunas condiciones, el ciclo finaliza, lo que significa que RunLoop también finaliza. Estas condiciones incluyen que la fuente se procesa y RunLoop debe detenerse después de procesar la fuente, el tiempo de espera de RunLoop, el modo actual de RunLoop se detiene y el elemento de modo en el modo actual está vacío. __CFRunLoopServiceMachPort se utiliza para manejar el modo de suspensión y activación.
Según la descripción del sitio web oficial , cada vez que se inicia RunLoop, RunLoop del hilo procesará algunos eventos pendientes y generará notificaciones para los observadores registrados. El orden de procesamiento es específico:
1. Notificar a los observadores: ingrese a RunLoop.
2. Notificar a los observadores: el cronómetro está a punto de ser procesado.
3. Notificar a los observadores: la fuente0 está a punto de ser procesada.
4. Procesar la fuente0 que está lista. Si no se procesa, se pondrá en suspensión. Si se procesa
, no se suspenderá. 5. Si es el bucle de ejecución de el hilo principal y el hilo principal tiene eventos para procesar, salte al paso 9. paso, es decir, no dormirá, pero si no hay eventos para procesar, también entrará en suspensión.
6. Notificar a los observadores: el hilo está a punto de dormirse
7. Hacer que el hilo duerma hasta que ocurra cualquiera de los siguientes eventos.

  • ocurre el evento fuente1
  • disparador del temporizador
  • RunLoop se agota
  • RunLoop se activa manualmente

8. Notificar a los observadores: el hilo se despierta
9. Procesar eventos pendientes

  • Si se activa el temporizador definido por el usuario, maneje el temporizador y reinicie el ciclo, vaya a 2
  • Si actualmente es el bucle de ejecución del hilo principal, procesando eventos del hilo principal, la fuente es solo 5
  • ocurre source1, maneja el evento. (Si se llama a CFRunLoopRun, vaya a 2; si se llama a CFRunLoopRunInMode, dependiendo del último parámetro, si se debe finalizar el ciclo o saltar a 2 para continuar el ciclo)
  • Si RunLoop se activa manualmente y no se agota el tiempo de espera, reinicie el ciclo y vaya a 2

10. Notificar a los observadores: RunLoop ha salido

Es posible que la imagen a continuación no sea precisa, consulte el resumen anterior
Lógica de procesamiento RunLoop
Nota: No hay fuente0 en el área verde de la imagen de la izquierda.

versión más detallada

[Error en la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-leeching, se recomienda guardar la imagen y cargarla directamente (img-yysFnppD-1652108715762) (http://mrpeak.cn/images/rl00.png )] En la imagen de arriba,
performTask y callout_to_observer se distinguen por diferentes colores. En la figura, puede ver intuitivamente cómo los 5 tipos de performTask y los 6 tipos de callout_to_observer se distribuyen en un bucle.

Algunos detalles son difíciles de reflejar en la imagen, así que los explicaré por separado.

¿Encuesta?

Cada vez que el bucle procesa la tarea source0 o RunLoop se agota, el valor de la encuesta será verdadero. El impacto directo es que no habrá DoObservers-BeforeWaiting y DoObservers-AfterWaiting, lo que significa que el runloop no entrará en modo de suspensión, por lo que no habrá BeforeWaiting y AfterWaiting, estas dos actividades.

mach_msg dos veces

De hecho, hay dos llamadas a mach_msg en un bucle. Una llamada sin marcar ocurre después de DoSource0, que leerá activamente la cola de mensajes relacionada con mainQueue. Sin embargo, esta llamada a mach_msg no entrará en suspensión porque se pasa el valor de tiempo de espera. La entrada es 0. Si se lee el mensaje, vaya directamente al código DoMainQueue. Este diseño debe garantizar que el código enviado a la cola principal siempre tenga una mayor probabilidad de ejecutarse.

Tipo de puerto

Cada vez que se activa el runloop, decidirá qué tipo de tarea ejecutar según el tipo de puerto. Solo se ejecutará uno de DoMainQueue, DoTimers y DoSource1, y el resto se dejará para el siguiente bucle para su ejecución.

6.2 Dormir y despertar de RunLoop

De hecho, para Event Loop, lo principal de RunLoop es garantizar que el subproceso entre en suspensión cuando no hay un mensaje para evitar ocupar recursos del sistema y pueda reactivarse a tiempo cuando hay un mensaje. Este mecanismo de RunLoop se basa completamente en el núcleo del sistema, específicamente en Mach en Darwin, el componente central del sistema operativo de Apple (Darwin es de código abierto). Mach se puede encontrar en el kernel inferior en la siguiente figura:

Mach es el núcleo de Darwin, que se puede decir que es el núcleo del kernel y proporciona servicios básicos como la comunicación entre procesos (IPC) y la programación del procesador. En Mach, la comunicación entre procesos y subprocesos se completa en forma de mensajes, y los mensajes se transmiten entre dos puertos (es por eso que Fuente1 se llama Fuente basada en puerto, porque depende de que el sistema envíe un mensaje al puerto especificado). ). La función mach_msg() en <mach/message.h> se usa para enviar y recibir mensajes (de hecho, Apple proporciona muy pocas API de Mach y no nos recomienda llamar a estas API directamente):

La esencia de mach_msg() es una llamada a mach_msg_trap(), que es equivalente a una llamada al sistema y activa el cambio de estado del kernel. Cuando el programa está en reposo, RunLoop permanece en __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll? 0: TIMEOUT_INFINITY, &voucherState, &voucherCopy), y dentro de esta función, se llama a mach_msg para poner el programa en estado de suspensión.

__CFRunLoopServiceMachPort:

/**
 *  接收指定内核端口的消息
 *
 *  @param port        接收消息的端口
 *  @param buffer      消息缓冲区
 *  @param buffer_size 消息缓冲区大小
 *  @param livePort    暂且理解为活动的端口,接收消息成功时候值为msg->msgh_local_port,超时时为MACH_PORT_NULL
 *  @param timeout     超时时间,单位是ms,如果超时,则RunLoop进入休眠状态
 *
 *  @return 接收消息成功时返回true 其他情况返回false
 */
static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout) {
    Boolean originalBuffer = true;
    kern_return_t ret = KERN_SUCCESS;
    for (;;) {      /* In that sleep of death what nightmares may come ... */
        mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
        msg->msgh_bits = 0;  //消息头的标志位
        msg->msgh_local_port = port;  //源(发出的消息)或者目标(接收的消息)
        msg->msgh_remote_port = MACH_PORT_NULL; //目标(发出的消息)或者源(接收的消息)
        msg->msgh_size = buffer_size;  //消息缓冲区大小,单位是字节
        msg->msgh_id = 0;  //唯一id
       
        if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
        
        //通过mach_msg发送或者接收的消息都是指针,
        //如果直接发送或者接收消息体,会频繁进行内存复制,损耗性能
        //所以XNU使用了单一内核的方式来解决该问题,所有内核组件都共享同一个地址空间,因此传递消息时候只需要传递消息的指针
        ret = mach_msg(msg,
                       MACH_RCV_MSG|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV),
                       0,
                       msg->msgh_size,
                       port,
                       timeout,
                       MACH_PORT_NULL);
        CFRUNLOOP_WAKEUP(ret);
        
        //接收/发送消息成功,给livePort赋值为msgh_local_port
        if (MACH_MSG_SUCCESS == ret) {
            *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
            return true;
        }
        
        //MACH_RCV_TIMEOUT
        //超出timeout时间没有收到消息,返回MACH_RCV_TIMED_OUT
        //此时释放缓冲区,把livePort赋值为MACH_PORT_NULL
        if (MACH_RCV_TIMED_OUT == ret) {
            if (!originalBuffer) free(msg);
            *buffer = NULL;
            *livePort = MACH_PORT_NULL;
            return false;
        }
        
        //MACH_RCV_LARGE
        //如果接收缓冲区太小,则将过大的消息放在队列中,并且出错返回MACH_RCV_TOO_LARGE,
        //这种情况下,只返回消息头,调用者可以分配更多的内存
        if (MACH_RCV_TOO_LARGE != ret) break;
        //此处给buffer分配更大内存
        buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
        if (originalBuffer) *buffer = NULL;
        originalBuffer = false;
        *buffer = realloc(*buffer, buffer_size);
    }
    HALT;
    return false;
}

mach_msg
Primero comprendamos los parámetros de entrada de mach_msg:
el primer parámetro es enviar el mensaje de contenido del mensaje. La estructura del mensaje define los puertos y otros contenidos del remitente y del receptor del mensaje; el segundo
parámetro pertenece al tipo de envío o recepción del mensaje. , el tipo se ha definido a través de la definición de macro, el envío es MACH_SEND_MSG, la recepción es MACH_RCV_MSG;
el receptor debe usar el tercer parámetro para solicitar espacio de almacenamiento adicional para almacenar temporalmente el mensaje, de modo que pueda procesarse por sí mismo sin acoplarse a el espacio del mensaje de origen; el
penúltimo parámetro Cada parámetro representa el tiempo de espera. Si es 0, significa regresar inmediatamente después de enviar o recibir. Si es TIMEOUT_INFINITY, se bloqueará y esperará un mensaje. El hilo actual siempre estará en estado inactivo hasta que haya un mensaje correspondiente al puerto correspondiente al parámetro 1. No regresará y continuará ejecutando los siguientes pasos: código;

El puerto mach
menciona el puerto muchas veces en runloop. Por ejemplo, la fuente1 basada en puerto es una de las fuentes de activación durante el modo de suspensión. Por ejemplo, el mensaje __CFRunLoopServiceMachPort también se monitorea durante el modo de suspensión a través del puerto.
¿Qué es entonces un puerto? Los mensajes Mach se pasan entre puertos. Un puerto puede tener sólo un receptor, pero puede tener varios remitentes al mismo tiempo. Enviar un mensaje a un puerto en realidad coloca el mensaje en una cola de mensajes hasta que el receptor pueda procesarlo.
Se puede ver en el código fuente que el tipo de puerto es __CFPort /mach_port_name_t /mach_port_t, y mach_port_name_t es un entero sin signo, que es el valor de índice del puerto. Hay varios tipos de puertos involucrados en el código fuente:

// 这个port就对应NSTimer;
    mach_port_t _timerPort;
    
// 这个port对应主线程
    mach_port_name_t dispatchPort = MACH_PORT_NULL;
    dispatchPort = _dispatch_get_main_queue_port_4CF();
    
// 这个port唤醒runloop
if (livePort == rl->_wakeUpPort)

¿Aún recuerdas la recopilación de puertos monitoreada durante el sueño en el método __CFRunLoopRun?

// 第七步,进入循环开始不断的读取端口信息,如果端口有唤醒信息则唤醒当前runLoop
__CFPortSet waitSet = rlm->_portSet;
...
...
if (kCFUseCollectableAllocator) 
{
    memset(msg_buffer, 0, sizeof(msg_buffer));
}

// waitSet 为所有需要监听的port集合, TIMEOUT_INFINITY表示一直等待
msg = (mach_msg_header_t *)msg_buffer;
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

El conjunto de espera aquí es __CFPortSet, que es una colección de puertos, entonces, ¿qué tipo es __CFPortSet? ¿Qué operaciones intervienen en esta colección?

typedef mach_port_t __CFPortSet;
...
...
CF_INLINE kern_return_t __CFPortSetInsert(__CFPort port, __CFPortSet portSet) {
    if (MACH_PORT_NULL == port) {
        return -1;
    }
    return mach_port_insert_member(mach_task_self(), port, portSet);
}

En otras palabras, el tipo de __CFPortSet también es mach_port_t, que es un entero sin signo. Luego, se supone que la operación __CFPortSetInsert se realiza mediante bits, y diferentes bits representan diferentes tipos de puertos. El parámetro de tipo __CFPort de __CFRunLoopServiceMachPort también se puede pasar a waitSet, y cada bit se atravesará internamente para monitorear los mensajes de cada puerto.
Además, ¿cómo se determina el conjunto de puertos sondeados durante la fase de suspensión del bucle de ejecución? A través del código fuente, encontramos que es el método __CFRunLoopFindMode el que inserta cada puerto en waitSet:

static CFRunLoopModeRef __CFRunLoopFindMode(CFRunLoopRef rl, CFStringRef modeName, Boolean create)
 {
    ...
    ...
    kern_return_t ret = KERN_SUCCESS;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    rlm->_timerFired = false;
    rlm->_queue = _dispatch_runloop_root_queue_create_4CF("Run Loop Mode Queue", 0);
    mach_port_t queuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
    if (queuePort == MACH_PORT_NULL) CRASH("*** Unable to create run loop mode queue port. (%d) ***", -1);
    rlm->_timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, rlm->_queue);
    
    __block Boolean *timerFiredPointer = &(rlm->_timerFired);
    dispatch_source_set_event_handler(rlm->_timerSource, ^{
        *timerFiredPointer = true;
    });
    
    // Set timer to far out there. The unique leeway makes this timer easy to spot in debug output.
    _dispatch_source_set_runloop_timer_4CF(rlm->_timerSource, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 321);
    dispatch_resume(rlm->_timerSource);
    
    ret = __CFPortSetInsert(queuePort, rlm->_portSet);
    if (KERN_SUCCESS != ret) CRASH("*** Unable to insert timer port into port set. (%d) ***", ret);
    
#endif
#if USE_MK_TIMER_TOO
    rlm->_timerPort = mk_timer_create();
    ret = __CFPortSetInsert(rlm->_timerPort, rlm->_portSet);
    if (KERN_SUCCESS != ret) CRASH("*** Unable to insert timer port into port set. (%d) ***", ret);
#endif
    
    ret = __CFPortSetInsert(rl->_wakeUpPort, rlm->_portSet);
    if (KERN_SUCCESS != ret) CRASH("*** Unable to insert wake up port into port set. (%d) ***", ret);
  
    CFSetAddValue(rl->_modes, rlm);
    CFRelease(rlm);
    __CFRunLoopModeLock(rlm);   /* return mode locked */
    return rlm;
}

De los tres __CFPortSetInsert anteriores, podemos encontrar que queuePort, _timerPort y _wakeUpPort se insertan respectivamente; además, el puerto de source1 se inserta en el método CFRunLoopAddSource:

......
__CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
if (CFPORT_NULL != src_port) 
{
    CFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls);
    __CFPortSetInsert(src_port, rlm->_portSet);
}
......

Junto con el despachoPort agregado en el método __CFRunLoopRun, waitSet ya contiene todos los puertos que pueden activar el runloop.

6.3 Procesamiento de tiempo de espera de RunLoop

En el método principal __CFRunLoopRun del código fuente del runloop, el despacho se usa para iniciar un temporizador antes de ingresar al ciclo principal do while.

El tiempo de espera se calcula en función de los segundos del parámetro de entrada de __CFRunLoopRun. ¿De dónde proviene el parámetro de entrada de __CFRunLoopRun? Puede encontrar CFRunLoopRunSpecific siguiendo el código fuente. El tiempo de espera predeterminado establecido por la persona que llama CFRunLoopRun es 1.0e10 y es necesario personalizar CFRunLoopRunInMode.

Cuando el temporizador de despacho alcanza el tiempo de espera, se llamará a la función de devolución de llamada configurada en despacho_source_set_event_handler_f (también puede usar despacho_source_set_event_handler para configurar el bloque). La función de devolución de llamada aquí es __CFRunLoopTimeout. El código fuente es el siguiente:

static void __CFRunLoopTimeout(void *arg) {
    struct __timeout_context *context = (struct __timeout_context *)arg;
    context->termTSR = 0ULL;
    CFRUNLOOP_WAKEUP_FOR_TIMEOUT();// 没啥X用
    CFRunLoopWakeUp(context->rl);
    // The interval is DISPATCH_TIME_FOREVER, so this won't fire again
}
void CFRunLoopWakeUp(CFRunLoopRef rl) {
    ......
    ret = __CFSendTrivialMachMessage(rl->_wakeUpPort, 0, MACH_SEND_TIMEOUT, 0);
    if (ret != MACH_MSG_SUCCESS && ret != MACH_SEND_TIMED_OUT) CRASH("*** Unable to send message to wake up port. (%d) ***", ret);
    ......
}
static uint32_t __CFSendTrivialMachMessage(mach_port_t port, uint32_t msg_id, CFOptionFlags options, uint32_t timeout) {
    kern_return_t result;
    mach_msg_header_t header;
    ......
    result = mach_msg(&header, MACH_SEND_MSG|options, header.msgh_size, 0, MACH_PORT_NULL, timeout, MACH_PORT_NULL);
    if (result == MACH_SEND_TIMED_OUT) mach_msg_destroy(&header);
    return result;
}

Del código fuente anterior, podemos ver que cuando llega el tiempo de espera, lo principal es llamar a mach_msg para enviar el mensaje a través de __CFSendTrivialMachMessage. El parámetro mach_msg se ha configurado con "modo de envío", "tiempo de espera" y el El puerto de activación es "rl->_wakeUpPort", según el artículo "¿Entiendes correctamente runloop?", podemos saber que cuando runloop está inactivo, cuando recibe un mensaje de mach, juzgará el puerto y decidirá. qué juicio y procesamiento hacer:

if (MACH_PORT_NULL == livePort)
{
      CFRUNLOOP_WAKEUP_FOR_NOTHING();
}
else if (livePort == rl->_wakeUpPort)
{
      CFRUNLOOP_WAKEUP_FOR_WAKEUP();
}
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort)
{
      // 处理timer
}
else if (livePort == dispatchPort) 
{
      ......
      // 处理主线程队列中事件
      __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
      ......
}
else 
{
      ......
      // 处理Source1
      sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
      ......
}

Es decir, podemos usar el temporizador de despacho para pasar el mensaje de tiempo de espera a través de mach, activar el runloop y luego ejecutar la rama else if (livePort == rl->_wakeUpPort) para manejar el tiempo de espera (CFRUNLOOP_WAKEUP_FOR_WAKEUP en la fuente actual El código es solo una definición de macro vacía y no se ha realizado ningún procesamiento). Entonces, este temporizador se usa para establecer contexto->termTSR = 0ULL y activar el ciclo de ejecución después del tiempo especificado. En el siguiente código, contexto->termTSR se usa para determinar si se agota el tiempo de espera y, si se agota, salir de RunLoop.

###6.4 Observador de RunLoop
El llamado Runloop, en resumen, es un mecanismo de ejecución diseñado por Apple que programa continuamente varias tareas en el hilo actual.

Cada vez que se ejecuta el bucle, hace principalmente tres cosas:

  • llamada_al_observador()
  • dormir()
  • realizar tareas()

Ya hemos hablado del sueño antes y continuaremos hablando de performTask en la parte posterior, que trata principalmente del observador.

Ya sabemos que RunLoop tiene seis estados: Runloop usa callout_to_observer para notificar al observador externo que se ha ejecutado una determinada tarea externa o en qué estado se encuentra actualmente el runloop.

Temporizador DoObservers

Como sugiere el nombre, se llama a DoObservers-Timer para informar a los observadores interesados ​​antes de ejecutar DoTimers. runloop notifica al observador a través de la siguiente función:

__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);

DoObservers-Fuente0

De la misma manera, se llama a DoObservers-Sources para informar a los observadores interesados ​​antes de ejecutar source0. runloop notifica al observador a través de la siguiente función:

__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

Entre las cinco formas anteriores de ejecutar tareas, dos de ellas pueden registrar al observador, pero las otras no son compatibles, incluidas mainQueue, source1 y block. Pero por ahora, no está claro si DoTimers se ejecutará después de que se envíe la notificación kCFRunLoopBeforeTimers.


El runloop utiliza DoObservers-Activity para notificar al mundo exterior su estado actual. ¿Qué actividad está ejecutando actualmente el runloop? ¿Cuántas actividades hay en total? Consulte el código fuente para mayor claridad:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

Sin más, déjame explicarte uno por uno:

kCFRunLoopEntry

Cada vez que el runloop vuelve a ingresar a la actividad, cada vez que el runloop ingresa a un modo, notifica al kCFRunLoopEntry externo una vez, y luego continuará ejecutándose en ese modo hasta que finalice el modo actual, luego cambie a otros modos y notifique kCFRunLoopEntry nuevamente.

kCFRunLoopBeforeTimers

Este es el DoObservers-Timer mencionado anteriormente. Apple probablemente clasificó la llamada del temporizador como una actividad en aras de la claridad del código. Su significado se ha introducido anteriormente y no se repetirá.

kCFRunLoopBeforeSources

De manera similar, Apple clasifica la llamada de origen como una actividad de bucle de ejecución.

kCFRunLoopBeforeWaiting

Esta actividad indica que el hilo actual puede entrar en suspensión. Si el mensaje se puede leer desde la cola del kernel, la tarea continuará ejecutándose. Si no hay mensajes adicionales en la cola actual, entrará en estado de suspensión. La función para leer mensajes es:

__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), poll ? 0 : TIMEOUT_INFINITY);

La esencia es llamar a la función del kernel mach_msg, prestar atención al valor del tiempo de espera, TIMEOUT_INFINITY indica que es posible entrar en el estado de suspensión indefinidamente.

kCFRunLoopAfterWaiting

Esta actividad ocurre cuando el hilo actual se reanuda desde el estado de suspensión, lo que significa que el mach_msg anterior finalmente lee el mensaje de la cola y puede continuar realizando tareas. Esta es una actividad que debe llamarse cada vez que el runloop se reanuda desde el estado inactivo. Si desea diseñar una herramienta para detectar el ciclo de ejecución del runloop, entonces esta actividad se puede utilizar como el comienzo del ciclo.

kCFRunLoopSalir

exit No hace falta decir que esta actividad se llamará al salir del RunLoop del modo actual.

Las devoluciones de llamadas de actividades no son solo para desarrolladores. De hecho, el sistema también completará algunas tareas registrando devoluciones de llamadas para actividades relacionadas. Por ejemplo, he visto la siguiente pila de llamadas:

...
__CFRunLoopDoObservers
...
[UIView(Hierarchy) addSubview:] 
...

Obviamente, después de que el sistema observe que el runloop ingresa a una actividad, realizará algún trabajo de diseño de UIView.

Mira esto de nuevo:

...
__CFRunLoopDoObservers
...
[UIViewController __viewWillDisappear:] 
...

Este es el sistema que utiliza DoObservers para pasar la devolución de llamada viewWillDisappear.

###6.5 Procesamiento de tareas RunLoop

Hemos discutido callout_to_observer y sleep antes, y también hemos hablado antes de performTask, aquí continuamos analizando estas Tareas. Desde el código fuente podemos ver un tipo de función Do:

__CFRunLoopDoBlocks(内部调用callout_to_block)
__CFRunLoopDoSources0(内部循环调用callout_to_source0_perform_function)
__CFRunLoopDoSource1(内部调用callout_to_source1_perform_function)
__CFRunLoopDoTimers(内部调用__CFRunLoopDoTimer)

__CFRunLoopDoObservers(观察者,非任务,内部callout_to_observer)

下面这个不是Do系列的,但是因为和callout系列并列,是任务的一种,也列出来
_dispatch_main_queue_callback_4CF(被servicing_main_dispatch_queue所调用)


Los desarrolladores pueden utilizar DoBlocks() y es muy sencillo de utilizar. Primero puede insertar un bloque en la cola de destino a través de CFRunLoopPerformBlock. La firma de la función es la siguiente:

voidCFRunLoopPerformBlock(CFRunLoopRef rl, modo CFTypeRef, void(^block)( void));

Para un uso detallado, consulte el documento: https://developer.apple.com/documentation/corefoundation/1542985-cfrunloopperformblock?language=objc

Se puede ver que cuando el bloque se inserta en la cola, está vinculado a un determinado modo de bucle de ejecución. Después de llamar a la API anterior, cuando se ejecuta runloop, ejecutará todos los bloques en la cola a través de la siguiente API:

__CFRunLoopDoBlocks(rl, rlm);

Obviamente, durante la ejecución solo se ejecutan todos los bloques relacionados con un determinado modo. En cuanto al momento de ejecución, hay muchos puntos, que también se marcarán más adelante.

DoSources0()
Sabemos que la fuente se usa para generar eventos asincrónicos. Hay dos fuentes en Runloop, fuente0 y fuente1. Aunque los nombres son similares, los mecanismos operativos de las dos son diferentes. La aplicación debe administrar manualmente Source0. Cuando Source0 esté listo para activarse, se debe llamar a CFRunLoopSourceSignal para indicarle a RunLoop que está listo para activarse. CFSocket está actualmente implementado como source0.

Para obtener documentación detallada, consulte: https://developer.apple.com/documentation/corefoundation/1542679-cfrunloopsourcecreate?language=objc

Después del enlace, cuando se ejecuta runloop, ejecutará todo source0 a través de la siguiente API:

__CFRunLoopDoSources0(rl, rlm, stopAfterHandle);

De la misma forma, cada vez que se ejecuta, solo se ejecutará source0 relacionado con el modo actual.

DoSource1()
source1 es administrado por RunLoop y el kernel. El principio de implementación de source1 se basa en la función mach_msg. Cuando llega un mensaje al puerto Mach de source1, el kernel enviará activamente una señal y determinará la tarea a ejecutar leyendo el mensaje en la cola de mensajes del kernel en un puerto determinado. CFMachPort y CFMessagePort se implementan con source1.

Para obtener documentación detallada, consulte: https://developer.apple.com/documentation/corefoundation/1542679-cfrunloopsourcecreate?language=objc

Después del enlace, cuando se ejecuta runloop, ejecutará una determinada fuente1 a través de la siguiente API:

__CFRunLoopDoSource1(rl, rlm, stopAfterHandle);

De la misma forma, cada vez que se ejecuta, solo se ejecutará source0 relacionado con el modo actual.

Hacer temporizadores()

Esto es relativamente simple. Los desarrolladores pueden usar las API relacionadas con NSTimer para registrar las tareas a ejecutar. Runloop realiza tareas relacionadas a través de las siguientes API:

__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());

De la misma forma, cada vez que se ejecute, solo se ejecutará el temporizador relacionado con el modo actual.

DoMainQueue()

Los desarrolladores llaman a la API de GCD para colocar tareas en la cola principal y runloop ejecuta las tareas programadas a través de la siguiente API:

_dispatch_main_queue_callback_4CF(msg);

Tenga en cuenta que aquí no hay ningún parámetro rlm, lo que significa que DoMainQueue y el modo runloop son irrelevantes. msg es el mensaje leído desde un determinado puerto a través de la función mach_msg.

Anteriormente enumeramos cinco formas de realizar tareas en RunLoop. Se puede ver que Apple las usará en diferentes escenarios. No resumiremos aquí, simplemente enumeraremos los ejemplos de Apple:

source0:
...
__CFRunLoopDoSources0     
...
[UIApplication sendEvent:] 
...
显然是系统用 source0 任务来接收硬件事件。
...
__CFRunLoopDoSources0
...
CA::Transaction::commit() 
...
这是系统在使用 doSource0 来提交 Core Animation 的绘制任务。

mainqueue:
...
_dispatch_main_queue_callback_4CF
...
[UIView(Hierarchy) _makeSubtreePerformSelector:withObject:withObject:copySublayers:]
...
系统在使用 doMainQueue 来执行 UIView 的布局任务。

timer:
...
__CFRunLoopDoTimer
...
[UICollectionView _updateWithItems:tentativelyForReordering:animator:]
...
这是系统在使用 doTimers 来 UI 绘制任务。

doBlocks:
...
__CFRunLoopDoBlocks
...
CA::Context::commit_transaction(CA::Transaction*)
...
这是系统在使用 doBlocks 来提交 Core Animation 的绘制任务。

7. Funciones implementadas por Apple usando RunLoop

7.1 Grupo de liberación automática

Después de que se inicia la aplicación, Apple registra dos observadores en el hilo principal RunLoop y sus devoluciones de llamada son todas _wrapRunLoopWithAutoreleasePoolHandler().

El primer evento monitoreado por el observador es la entrada (a punto de ingresar al bucle), y se llamará a _objc_autoreleasePoolPush() en su devolución de llamada para crear un grupo de liberación automática. Su orden es -2147483647, que tiene la máxima prioridad y garantiza que la creación del grupo de lanzamiento se produzca antes que todas las demás devoluciones de llamada.

El segundo observador monitorea dos eventos: _objc_autoreleasePoolPop() y _objc_autoreleasePoolPush() se llaman cuando BeforeWaiting (preparándose para entrar en suspensión) para liberar el grupo antiguo y crear uno nuevo; _objc_autoreleasePoolPop() se llama cuando Exit (a punto de salir del bucle) para liberar el grupo de liberación automática. El orden de este observador es 2147483647, con la prioridad más baja, lo que garantiza que su grupo de lanzamiento se produzca después de todas las demás devoluciones de llamada.

El código ejecutado en el hilo principal generalmente se escribe en devoluciones de llamadas de eventos y devoluciones de llamadas de temporizador. Estas devoluciones de llamada estarán rodeadas por el AutoreleasePool creado por RunLoop, por lo que no habrá pérdidas de memoria y los desarrolladores no tendrán que crear el Pool explícitamente.

7.2 Respuesta al incidente

Apple ha registrado un Source1 (basado en el puerto mach) para recibir eventos del sistema y su función de devolución de llamada es __IOHIDEventSystemClientQueueCallback().

Cuando ocurre un evento de hardware (tocar/bloquear pantalla/sacudir, etc.), IOKit.framework genera primero un evento IOHIDEvent y SpringBoard lo recibe. Los detalles de este proceso se pueden encontrar aquí. SpringBoard solo recibe varios eventos, como presionar botones (pantalla de bloqueo/silencio, etc.), toque, aceleración, sensor de proximidad, etc., y luego los reenvía al proceso de aplicación requerido usando el puerto mach. Luego, el Source1 registrado por Apple activará la devolución de llamada y llamará a _UIApplicationHandleEventQueue() para su distribución dentro de la aplicación.

_UIApplicationHandleEventQueue() procesará y empaquetará IOHIDEvent en UIEvent para su procesamiento o distribución, incluida la identificación de UIGesture/procesamiento de rotación de pantalla/envío a UIWindow, etc. Por lo general, eventos como clics de UIButton y toques de inicio/mover/finalizar/cancelar se completan en esta devolución de llamada.

7.3 Reconocimiento de gestos

Cuando el _UIApplicationHandleEventQueue() anterior reconoce un gesto, primero llamará a Cancelar para interrumpir las devoluciones de llamada de la serie touchesBegin/Move/End actuales. Luego, el sistema marca el UIGestureRecognizer correspondiente como pendiente.

Apple ha registrado un observador para monitorear el evento BeforeWaiting (el bucle está a punto de dormirse). La función de devolución de llamada de este observador es _UIGestureRecognizerUpdateObserver (), que obtiene internamente todos los GestureRecognizers que acaban de marcarse como pendientes y ejecuta la devolución de llamada de GestureRecognizer.

Cuando hay cambios en UIGestureRecognizer (creación/destrucción/cambio de estado), esta devolución de llamada se procesará en consecuencia.

7.4 Actualización de la interfaz

Al operar la interfaz de usuario, como cambiar el marco, actualizar la jerarquía de UIView/CALayer o llamar manualmente al método setNeedsLayout/setNeedsDisplay de UIView/CALayer, UIView/CALayer se marca como pendiente y se envía a un Ir al contenedor global.

Apple ha registrado un observador para escuchar los eventos BeforeWaiting (a punto de entrar en suspensión) y Exit (a punto de salir del bucle), y una devolución de llamada para ejecutar una función larga:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv(). Esta función atravesará todas las UIView/CAlayers pendientes para realizar dibujos y ajustes reales, y actualizar la interfaz de usuario.

La pila de llamadas dentro de esta función probablemente se vea así:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                        [CALayer layoutSublayers];
                            [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                        [CALayer display];
                            [UIView drawRect];

7,5 temporizador

NSTimer es en realidad CFRunLoopTimerRef y tienen un puente gratuito. Después de registrar un NSTimer en RunLoop, RunLoop registrará eventos para sus puntos de tiempo repetidos. Por ejemplo, estos puntos horarios son las 10:00, 10:10 y 10:20. Para ahorrar recursos, RunLoop no devolverá la llamada a este temporizador en un momento muy preciso. El temporizador tiene un atributo llamado Tolerancia, que indica el error máximo permitido cuando se alcanza el punto de tiempo.

Si se pierde un determinado momento, por ejemplo, se ejecuta una tarea larga, la devolución de llamada en ese momento también se omitirá y la ejecución no se retrasará. Al igual que esperando el autobús, si estoy ocupado jugando con mi teléfono a las 10:10 y pierdo el autobús a esa hora, entonces solo puedo esperar el autobús de las 10:20.

CADisplayLink es un temporizador consistente con la frecuencia de actualización de la pantalla (pero el principio de implementación real es más complejo y diferente de NSTimer. En realidad, opera una fuente internamente). Si se ejecuta una tarea larga entre dos actualizaciones de pantalla, se omitirá un fotograma (similar a NSTimer), lo que provocará que la interfaz se atasque. Al deslizar TableView rápidamente, el usuario notará incluso un cuadro de retraso. El AsyncDisplayLink de código abierto de Facebook es para resolver el problema del retraso de la interfaz, y RunLoop también se usa internamente.

7.6 RealizarSelector

Cuando se llama a performSelector:afterDelay: de NSObject, se creará un temporizador internamente y se agregará al RunLoop del hilo actual. Entonces, si el hilo actual no tiene RunLoop, este método fallará.

Cuando se llama a performSelector: onThread:, en realidad creará un source0 y lo agregará al hilo correspondiente. De manera similar, si el hilo correspondiente no tiene un RunLoop, este método también fallará.

7.7 Acerca del MCD

De hecho, GCD también se usa en la parte inferior de RunLoop. Por ejemplo, RunLoop puede implementar el temporizador a través de despacho_source_t (a pesar de que NSTimer se implementa preferentemente usando mk_timer del kernel XNU, el código fuente también incluye formas de implementarlo usando GCD) . Al mismo tiempo, algunas interfaces proporcionadas por GCD también usan RunLoop, como despacho_async ().

Cuando se llama a despacho_async(dispatch_get_main_queue(), block), libDispatch enviará un mensaje al RunLoop del hilo principal. RunLoop se activará, obtendrá el bloque del mensaje y ejecutará el bloque en la devolución de llamada CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE ( ) . Pero esta lógica se limita al envío al subproceso principal, y libDispatch aún maneja el envío a otros subprocesos.

7.8 Acerca de las solicitudes de red

En iOS, la interfaz para solicitudes de red tiene las siguientes capas de abajo hacia arriba:

CFSocket
CFNetwork       ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession    ->AFNetworking2, Alamofire

CFSocket es la interfaz de nivel más bajo y solo es responsable de la comunicación del socket.
• CFNetwork es una encapsulación de capa superior basada en interfaces como CFSocket y ASIHttpRequest funciona en esta capa.
• NSURLConnection es una encapsulación de nivel superior basada en CFNetwork, que proporciona una interfaz orientada a objetos. AFNetworking 1.0 funciona en esta capa.
• NSURLSession es una nueva interfaz en iOS 7. En la superficie, es paralela a NSURLConnection, pero la capa inferior todavía usa algunas funciones de NSURLConnection (como el hilo com.apple.NSURLConnectionLoader). AFNetworking 2, 3 y Alamofire funcionan en esta capa. .

A continuación se presenta principalmente el proceso de trabajo de NSURLConnection.

Por lo general, cuando utilice NSURLConnection, pasará un Delegado. Cuando se llame a [inicio de la conexión], este Delegado continuará recibiendo devoluciones de llamadas de eventos. De hecho, la función de inicio obtiene internamente CurrentRunLoop y luego agrega 4 Source0 (es decir, la fuente que debe activarse manualmente) en DefaultMode. CFMultiplexerSource es responsable de varias devoluciones de llamadas de delegados y CFHTTPCookieStorage es responsable de manejar varias cookies.

Cuando comienza la transferencia de red, podemos ver que NSURLConnection crea dos nuevos hilos: com.apple.NSURLConnectionLoader y com.apple.CFSocket.private. El hilo CFSocket maneja la conexión del socket subyacente. NSURLConnectionLoader Este hilo utilizará RunLoop internamente para recibir eventos del socket subyacente y notificar al Delegado superior a través del Source0 agregado previamente.

RunLoop en NSURLConnectionLoader recibe notificaciones del CFSocket subyacente a través de alguna fuente basada en puerto mach. Después de recibir la notificación, enviará notificaciones a Source0, como CFMultiplexerSource, en el momento adecuado y, al mismo tiempo, activará el RunLoop del subproceso Delegate para permitirle procesar estas notificaciones. CFMultiplexerSource realizará la devolución de llamada real al Delegado en el RunLoop del subproceso Delegado.

8. Ejemplos de aplicaciones prácticas de RunLoop

8.1 Redes AF

AFURLConnectionOperation Esta clase está construida en base a NSURLConnection, que espera recibir devoluciones de llamadas de delegados en el hilo de fondo. AFNetworking creó un hilo separado para este propósito e inició un RunLoop en este hilo:

+ (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;
}

Debe haber al menos un temporizador/observador/fuente dentro antes de que se inicie RunLoop, por lo que AFNetworking crea un nuevo NSMachPort y lo agrega antes de [ejecutar runLoop]. Normalmente, la persona que llama necesita mantener este NSMachPort (mach_port) y enviar mensajes al bucle a través de este puerto en un hilo externo; pero el puerto se agrega aquí solo para evitar que RunLoop salga y no se usa para enviar mensajes.

- (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];
}

Cuando se requiere este subproceso en segundo plano para realizar una tarea, AFNetworking lanza la tarea al RunLoop del subproceso en segundo plano llamando a [NSObject performSelector:onThread:...].

8.2 Kit de visualización asíncrono

AsyncDisplayKit es un marco lanzado por Facebook para mantener la fluidez de la interfaz y su principio es aproximadamente el siguiente:

Una vez que se producen tareas pesadas en el hilo de la interfaz de usuario, la interfaz se atasca. Dichas tareas generalmente se dividen en tres categorías: composición tipográfica, dibujo y manipulación de objetos de la interfaz de usuario.

La composición tipográfica generalmente incluye operaciones como calcular el tamaño de la vista, calcular la altura del texto y volver a calcular la composición tipográfica de diagramas de subfórmula.
El dibujo generalmente incluye dibujo de texto (como CoreText), dibujo de imágenes (como predescompresión), dibujo de elementos (Cuarzo) y otras operaciones.
Las operaciones de objetos de UI generalmente incluyen la creación, configuración de propiedades y destrucción de objetos de UI como UIView/CALayer.

Los dos primeros tipos de operaciones se pueden enviar al subproceso en segundo plano para su ejecución a través de varios métodos, mientras que el último tipo de operación solo se puede completar en el subproceso principal y, a veces, las operaciones posteriores deben depender de los resultados de operaciones anteriores (por ejemplo , cuando se crea un TextView, es posible que sea necesario calcular el texto con anticipación. Lo que hace ASDK es intentar poner en segundo plano las tareas que se pueden poner en segundo plano y tratar de posponer aquellas que no se pueden poner en segundo plano (como la creación de vistas y el ajuste de atributos).

Para este fin, ASDK crea un objeto llamado ASDisplayNode y encapsula UIView/CALayer internamente, tiene propiedades similares a UIView/CALayer, como marco, fondoColor, etc. Todas estas propiedades se pueden cambiar en el hilo de fondo. Los desarrolladores solo pueden operar UIView/CALayer interno a través de Node, por lo que la composición tipográfica y el dibujo se pueden colocar en el hilo de fondo. Pero no importa cómo opere, estas propiedades siempre deben sincronizarse con UIView/CALayer del hilo principal en algún momento.

ASDK sigue el patrón del marco QuartzCore/UIKit e implementa un mecanismo de actualización de interfaz similar: es decir, agregar un observador en el RunLoop del hilo principal, escuchar los eventos kCFRunLoopBeforeWaiting y kCFRunLoopExit, y al recibir la devolución de llamada, itera a través de todos los poner previamente en la cola de tareas pendientes y luego ejecutarlas una por una.
El código específico se puede ver aquí: _ASAsyncTransactionGroup.

He visto muchas aplicaciones del sistema RunLoop y el uso de algunas bibliotecas de terceros conocidas. Entonces, además de estas, ¿podemos usar RunLoop correctamente para ayudarnos a hacer algo durante el proceso de desarrollo real?

8.3 Otros

Para pensar en este problema, solo necesita mirar la relación de inclusión de RunLoopRef. RunLoop contiene múltiples modos y su modo se puede personalizar. Se puede inferir que ya sea Source1, Timer u Observer, los desarrolladores pueden usarlo, pero generalmente En este caso no se personalizará el Temporizador y mucho menos un Modo completo, lo que se utiliza más es en realidad el cambio entre Observador y Modo.
Por ejemplo, muchas personas están familiarizadas con el uso de performSelector para configurar imágenes en el modo predeterminado para evitar que UITableView se desplace ([[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 100, 100)] performSelector:@selector(setImage:) withObject: myImage afterDelay:0.0 inModes:@NSDefaultRunLoopMode]). También existe UITableView+FDTemplateLayoutCell de sunnyxx, que utiliza Observer para calcular la altura de UITableViewCell y almacenarlo en caché cuando la interfaz está inactiva. PerformanceMonitor de Lao Tan también utiliza Observer para monitorear RunLoop en tiempo real para monitorear el retraso de iOS.

El contenido de este artículo proviene del resumen de la siguiente publicación de blog:

Investigación en profundidad de iOS: comprensión profunda de RunLoop https://blog.ibireme.com/2015/05/18/runloop/
Desarrollo de iOS: explicación detallada de Runloop https://www.cnblogs.com/CrazyD0u/ p/6481092.html
Los principios subyacentes de iOS Resumen: RunLoop https://www.jianshu.com/p/de752066d0ad
Explicación detallada de iOS RunLoop https://www.jianshu.com/p/23e3ff9619c3
https://www. jianshu.com/p/18e45cbd564f
Hay mucha gente sobre runloop ¡Todos se equivocaron! https://www.jianshu.com/p/ae0118f968bf
Notas de interpretación del código fuente de Runloop https://www.jianshu.com/p/288e8abc80f1
Descifrado de Runloop http://mrpeak.cn/blog/ios-runloop/

Lectura recomendada:

Notas de estudio de iOS runloop (1) - Documento oficial https://www.jianshu.com/p/8e40a7b16357
Notas de estudio de iOS runloop (2) - video maestro de sunnyxx https://www.jianshu.com/p/929d855c5a5a
Estudio de iOS runloop Notas (3) - Resumen del video de Yanzu Ye Gucheng https://www.jianshu.com/p/924cb2b218f5
Notas de estudio de iOS runloop (4) - Resumen https://www.jianshu.com/p/6a41cd354dc6
Algunas cosas sobre performSelector Pequeña discusión https://juejin.im/post/5c637a1ff265da2dbc596ec2
Comprensión del anidamiento de runloop https://www.jianshu.com/p/318328c0a2d1
subprocesos múltiples: subproceso abandonado http://sindrilin.com/2018/04/14 /abandoned_thread .html

Supongo que te gusta

Origin blog.csdn.net/Mamong/article/details/124677766
Recomendado
Clasificación