RunLoop no iOS: o que é, modos de funcionamento e ciclo de eventos

Autor: IT Sectr Publicado: 2026-03-16 Tempo de leitura: 11 min

RunLoop — um ciclo de processamento de eventos no iOS, implementado pelos objetos CFRunLoop (Core Foundation) e NSRunLoop (Foundation). É um mecanismo que aguarda eventos (toques, timers, fontes de entrada, notificações) e os despacha para os manipuladores correspondentes na thread. Cada thread no iOS tem no máximo um RunLoop, mas ele é criado automaticamente apenas para a Main Thread. De acordo com a Apple CFRunLoop Documentation, o RunLoop é crítico para o funcionamento de timers, animações e monitoramento de fontes em threads em segundo plano.

Principais pontos

  • RunLoop — event loop que processa eventos na thread: toques, timers, fontes de entrada
  • Main Thread possui um RunLoop automático (CFRunLoopGetMain()), threads em segundo plano — é necessário iniciar manualmente
  • Três modos: .default (principal), .tracking (scroll), .common (combina default+tracking)
  • NSTimer e CADisplayLink não funcionam sem um RunLoop ativo na thread
  • RunLoop observers permitem reagir à entrada/saída dos modos e início/fim do processamento

O que é RunLoop

RunLoop — é um objeto de infraestrutura do Core Foundation que organiza o processamento de eventos na thread. Em essência, é um loop infinito (while true) que aguarda a chegada de eventos (sources) e os transmite aos manipuladores. Quando não há eventos, o RunLoop coloca a thread em modo de repouso (sleep), economizando energia da bateria. Quando um evento chega, a thread é despertada, processa o evento e volta a dormir. O RunLoop existe apenas no iOS/macOS (XNU + Core Foundation) — no Android, seu papel é desempenhado pelo Looper.

Cada thread tem no máximo um RunLoop, que é criado de forma lazy na primeira solicitação. Para a Main Thread, o RunLoop é criado automaticamente na inicialização do aplicativo. Para threads em segundo plano, o RunLoop não é criado até que CFRunLoopGetCurrent() ou RunLoop.current seja chamado. O RunLoop principal do aplicativo é responsável por processar eventos de toque, renderização de tela, execução de blocos DispatchQueue.main e manutenção das camadas do Core Animation.

RunLoop não é uma thread — é um mecanismo dentro da thread. Uma thread pode existir sem RunLoop (se executar uma tarefa síncrona e terminar), mas um RunLoop não pode existir sem uma thread. Quando uma thread com RunLoop ativo não tem eventos, ela não bloqueia a CPU, mas fica em estado de espera (waiting) — esta é uma diferença crucial de um loop busy-wait, que consome 100% da CPU.

Como funciona o RunLoop: anatomia do ciclo de eventos

RunLoop processa dois tipos de fontes de eventos: Input Sources (fontes de entrada) e Timer Sources (timers). Input Sources entregam eventos assíncronos: toques, movimentos do mouse, dados de socket, mensagens de outras threads (performSelector:onThread:). Timer Sources entregam eventos síncronos agendados: NSTimer, CADisplayLink. Também existem Observers — pontos de entrada para monitorar o estado do RunLoop.

O ciclo do RunLoop consiste em fases sequenciais: entrada no modo (kCFRunLoopEntry), processamento de timers (kCFRunLoopBeforeTimers), processamento de fontes de entrada (kCFRunLoopBeforeSources), processamento de fontes (kCFRunLoopAfterWaiting), espera (sleep), saída do modo (kCFRunLoopExit). Se nenhum evento for processado na iteração atual, o RunLoop coloca a thread em sleep por tempo indeterminado até ser despertado por um novo evento.

swift
import Foundation

// Demonstração das fases do RunLoop através do Observer
func observeRunLoopActivities() {
    let observer = CFRunLoopObserverCreateWithHandler(
        nil,
        CFOptionFlags([[.entry, .beforeTimers, .beforeSources,
                           .afterWaiting, .exit]]),
        true,           // repeats
        0               // priority
    ) { observer, activity in
        switch activity {
        case .entry:
            print("Entry — RunLoop ativado")
        case .beforeTimers:
            print("BeforeTimers — processamento de timers")
        case .beforeSources:
            print("BeforeSources — processamento de fontes")
        case .afterWaiting:
            print("AfterWaiting — despertar após sleep")
        case .exit:
            print("Exit — RunLoop finalizado")
        default:
            break
        }
    }

    CFRunLoopAddObserver(
        CFRunLoopGetCurrent(),
        observer,
        .commonModes
    )
}

// Exemplo: RunLoop processa um timer na thread principal
func timerOnMainRunLoop() {
    Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
        print("Tick: \(Date())")
    }

    // RunLoop.current.run() na Main Thread é chamado pelo UIApplicationMain
    // automaticamente — não precisa iniciar manualmente
    RunLoop.current.run() // Esta chamada não retorna na Main Thread
}

O exemplo observeRunLoopActivities registra um Observer no RunLoop principal que registra cada fase do ciclo. Isso é útil para depuração: se você vir um longo intervalo entre .beforeTimers e .afterWaiting, significa que o RunLoop está bloqueado por uma operação na Main Thread. timerOnMainRunLoop mostra como o NSTimer funciona automaticamente no RunLoop principal — ao criar Timer.scheduledTimer, o timer é adicionado ao RunLoop atual no modo padrão (.default mode).

RunLoop vs Looper (Android)

Android Looper — é o análogo do RunLoop. Looper.prepare() cria uma fila de mensagens (MessageQueue) na thread, Looper.loop() inicia o loop infinito de processamento. Handler envia mensagens e Runnable para esta fila. A principal diferença: o RunLoop suporta modos (modes), enquanto o Android Looper não. O Looper processa todas as mensagens sem filtragem por modo, o que o torna mais simples, mas menos flexível para cenários com prioridades (por exemplo, o scroll no iOS é processado no modo .tracking separadamente de outros eventos).

Modos do RunLoop: default, tracking, common

RunLoop Mode — é um conjunto de fontes, timers e observadores que estão ativos no momento. Os modos permitem isolar o processamento de eventos por prioridades. Quando o usuário faz scroll em uma UITableView, o RunLoop muda para o modo .tracking, no qual apenas eventos de scroll e timers/animações correspondentes são processados. Todas as outras fontes (por exemplo, NSURLConnection) são pausadas até a saída do modo de scroll.

Três modos principais: .default (NSDefaultRunLoopMode) — modo principal, no qual todos os eventos são processados, exceto scroll; .tracking (UITrackingRunLoopMode) — ativado durante scroll ou navegação por gestos; .common (NSRunLoopCommonModes) — não é um modo separado, mas um conjunto de aliases que inclui .default + .tracking. Adicionar uma fonte em .commonModes a adiciona automaticamente a todos os modos do conjunto.

ModoConstante Core FoundationConstante FoundationQuando está ativo
.defaultkCFRunLoopDefaultModeRunLoop.Mode.defaultEstado normal, sem scroll
.trackingUITrackingRunLoopModeRunLoop.Mode.trackingScroll, gesture recognizers
.commonkCFRunLoopCommonModesRunLoop.Mode.commonPseudo-modo: default + tracking
.initialRunkCFRunLoopInitialRunRunLoopModePrimeira execução do RunLoop

Por que o NSTimer não dispara durante o scroll

Problema clássico: NSTimer, adicionado no modo .default, para de disparar durante o scroll porque o RunLoop muda para o modo .tracking e não processa timers do .default. A solução — adicionar o timer em .commonModes: RunLoop.current.add(timer, forMode: .common). Isso faz com que o Timer dispare tanto no .default quanto no .tracking. Alternativa — usar DispatchQueue.main.async em vez de NSTimer, já que o GCD opera no nível das threads, não nos modos do RunLoop.

RunLoop em threads em segundo plano

Threads em segundo plano não têm RunLoop por padrão. Se em uma thread em segundo plano for necessário executar um NSTimer, processar NSInputStream/NSOutputStream ou reagir a performSelector:, é preciso criar e iniciar o RunLoop manualmente. Sem o RunLoop, o timer e o performSelector: nunca serão executados — a thread executará o código e terminará, sem aguardar eventos.

Para criar um RunLoop em uma thread em segundo plano, basta chamar RunLoop.current.run() no final do trabalho da thread. Esta chamada bloqueia a thread indefinidamente, processando eventos. Para interromper, use CFRunLoopStop(CFRunLoopGetCurrent()). Importante: RunLoop.current cria o RunLoop de forma lazy na primeira chamada — se run() não for chamado, ele não processará eventos. Padrão: configuração das fontes → adição ao RunLoop → chamada de run().

swift
import Foundation

// Thread em segundo plano com seu próprio RunLoop
class BackgroundRunLoopManager {

    private let thread: Thread
    private var isRunning = false

    init() {
        thread = Thread { [weak self] in
            // RunLoop é criado automaticamente ao chamar RunLoop.current
            let runLoop = RunLoop.current

            // Adicionamos uma porta para manter o RunLoop ativo
            runLoop.add(Port(), forMode: .default)

            // Iniciamos o processamento de eventos
            var isFinished = false
            while !isFinished {
                // run(mode:before:) retorna true se um evento foi processado
                isFinished = !runLoop.run(mode: .default, before: Date.distantFuture)
            }
        }
        thread.name = "com.app.background-runloop"
    }

    func start() {
        thread.start()
        isRunning = true
    }

    func stop() {
        // Parando o RunLoop na thread em segundo plano
        self.perform(
            #selector(BackgroundRunLoopManager.stopRunLoop),
            on: thread,
            with: nil,
            waitUntilDone: false
        )
    }

    @objc
    private func stopRunLoop() {
        CFRunLoopStop(CFRunLoopGetCurrent())
        isRunning = false
    }
}

// Uso: timer no RunLoop em segundo plano
let manager = BackgroundRunLoopManager()
manager.start()

// Enviando tarefa para o RunLoop em segundo plano via performSelector
manager.perform(
    #selector(BackgroundRunLoopManager.backgroundTask),
    on: manager.thread,
    with: nil,
    waitUntilDone: false
)

BackgroundRunLoopManager cria uma thread em segundo plano com RunLoop permanente. A adição de um Port() vazio é necessária para que o RunLoop não termine imediatamente — sem fontes, RunLoop.run() retorna false e sai. performSelector:onThread: envia uma mensagem para o RunLoop em segundo plano — ela será processada quando o RunLoop entrar na fase BeforeSources. Stop chama CFRunLoopStop na thread em segundo plano, finalizando o ciclo.

NSTimer cria um evento de timer que o RunLoop processa na fase BeforeTimers. Os timers podem ser repeating (repetitivos) e non-repeating (único). O NSTimer não garante precisão de disparo: se o RunLoop estiver bloqueado por uma operação longa, o timer disparará após o desbloqueio, e todos os disparos perdidos serão combinados em um (para timers repeating — no máximo um disparo “recuperado”).

CADisplayLink — é um timer especializado, sincronizado com a taxa de atualização da tela (60/120/144 Hz). Ele é usado para animações e atualização de vídeo. O CADisplayLink é adicionado ao RunLoop e dispara antes de cada quadro de renderização (antes que o Core Animation envie a camada para renderização). Se um quadro for perdido (o display link não conseguiu disparar em 16 ms), a próxima chamada ocorre no próximo ciclo VSync.

swift
import UIKit

class AnimationController {

    private var displayLink: CADisplayLink?
    private var displayLinkTimer: Timer?
    private var startTime: CFTimeInterval = 0

    // CADisplayLink — animação com vsync
    func startDisplayLinkAnimation() {
        displayLink = CADisplayLink(target: self,
                                       selector: #selector(step))
        // Adicionar no modo .common — funciona mesmo durante o scroll
        displayLink?.add(to: .current, forMode: .common)
        startTime = CACurrentMediaTime()
    }

    @objc
    private func step(displayLink: CADisplayLink) {
        let elapsed = CACurrentMediaTime() - startTime
        // Chamado a cada quadro (60 FPS → a cada 16.6 ms)
        print("Frame at \(elapsed) seconds")

        if elapsed > 5.0 {
            displayLink.invalidate() // parada após 5 segundos
        }
    }

    // NSTimer — tarefa periódica
    func startTimerInCommonMode() {
        displayLinkTimer?.invalidate()
        displayLinkTimer = Timer.scheduledTimer(
            withTimeInterval: 1.0,
            repeats: true
        ) { [weak self] timer in
            print("Timer tick")
        }

        // CHAVE: adicionamos em .common, caso contrário o timer congela no scroll
        RunLoop.current.add(displayLinkTimer!, forMode: .common)
    }

    func stop() {
        displayLink?.invalidate()
        displayLinkTimer?.invalidate()
    }
}

Em AnimationController, o CADisplayLink é adicionado no modo .common, o que garante que step seja chamado em cada quadro independentemente do scroll. displayLink.add(to: .current, forMode: .common) — padrão para animações que não devem ser interrompidas durante o scroll. O NSTimer também é adicionado no modo .common para que ele dispare durante o scroll. Sem isso, o timer dispararia apenas no modo .default.

Observers do RunLoop: monitoramento de eventos do ciclo

CFRunLoopObserver — mecanismo para monitorar as fases do RunLoop. Com um Observer, é possível receber notificações sobre entrada no modo, início do processamento de timers, início do processamento de fontes, despertar após sleep, saída do modo. Os Observers são usados por frameworks para suas necessidades: o Core Animation os usa para renderizar camadas antes do RunLoop dormir, o UIKit — para atualizar o layout após o processamento de eventos.

O desenvolvedor também pode adicionar Observers para seus próprios fins. Por exemplo: medir o tempo de processamento de eventos (perfilamento), executar operações adiadas antes do RunLoop dormir (quando a UI já foi atualizada e o usuário não está interagindo), salvar dados automaticamente durante inatividade prolongada. O Observer é registrado através de CFRunLoopAddObserver com a especificação do modo e uma máscara de bits das atividades monitoradas.

Os pontos mais úteis para um Observer: .afterWaiting — executado após o despertar do RunLoop e pode conter código que deve ser executado após o processamento do evento; .beforeTimers — antes do processamento de timers, permite medir o tempo decorrido desde o processamento anterior; .exit — dispara ao parar o RunLoop, útil para limpeza de recursos da thread em segundo plano.

CFRunLoopStop e finalização do ciclo

CFRunLoopStop — função que encerra forçadamente a iteração atual do RunLoop. Ao chamar CFRunLoopStop(CFRunLoopGetCurrent()), o RunLoop finaliza o processamento do evento atual e sai de run(), retornando false. Esta é a forma padrão de parar o RunLoop em uma thread em segundo plano. Na Main Thread, CFRunLoopStop não é recomendado — o RunLoop principal deve funcionar durante toda a vida do aplicativo. Para threads em segundo plano, após CFRunLoopStop a thread pode terminar ou continuar executando o próximo código após run().

Perguntas frequentes

O que é RunLoop no iOS?

RunLoop — ciclo de processamento de eventos no iOS, implementado por CFRunLoop (Core Foundation) e NSRunLoop (Foundation). Ele aguarda eventos (toques, timers, fontes de entrada) e os despacha para os manipuladores na thread. Cada thread pode ter um RunLoop, mas automaticamente ele é criado apenas para a Main Thread. O RunLoop gerencia modos (.default, .tracking, .common), isolando o processamento por prioridades.

Por que o NSTimer não funciona durante o scroll?

NSTimer por padrão é adicionado ao modo .default do RunLoop. Quando o usuário faz scroll, o RunLoop muda para o modo .tracking e não processa timers do .default. Solução: adicione o timer ao modo .common através de RunLoop.current.add(timer, forMode: .common). .common combina .default e .tracking, portanto o timer dispara em ambos os modos.

Preciso iniciar o RunLoop em uma thread em segundo plano?

Apenas se a thread em segundo plano usar timers (NSTimer), performSelector:onThread:, NSInputStream/NSOutputStream ou eventos Source. Se a thread executar uma tarefa síncrona (download de arquivo, cálculos) e terminar — o RunLoop não é necessário. Para iniciar, chame RunLoop.current.run() após configurar as fontes. Para parar — CFRunLoopStop(CFRunLoopGetCurrent()).

Qual a diferença entre RunLoop e GCD DispatchQueue?

RunLoop opera no nível da thread e processa eventos sequencialmente, com suporte a modos. DispatchQueue — abstração de pool de threads, as tarefas são executadas em qualquer thread livre. O GCD não suporta modos e vive independentemente do RunLoop. DispatchQueue.main usa o RunLoop principal para executar blocos — este é o único ponto de interseção. Para tarefas em segundo plano, o GCD é preferível.

Como o CADisplayLink está relacionado ao RunLoop?

CADisplayLink — é um timer sincronizado com o VSync (taxa de atualização da tela). Ele é adicionado ao RunLoop e dispara antes de cada quadro de renderização, na fase BeforeTimers. O CADisplayLink funciona apenas na Main Thread, pois a renderização da tela ocorre lá. Para animações contínuas durante o scroll, adicione-o no modo .common: displayLink.add(to: .current, forMode: .common).

Resumo

  • RunLoop — event loop do iOS: processa toques, timers, fontes de entrada na thread; a Main Thread tem um RunLoop automático
  • Três modos: .default (geral), .tracking (scroll), .common (default + tracking) — gerenciam a filtragem de eventos
  • NSTimer no .default não dispara durante o scroll — solução: adicionar ao modo .common via RunLoop.current.add(timer, forMode: .common)
  • Threads em segundo plano não têm RunLoop por padrão — para timers e performSelector: é necessário iniciar manualmente com RunLoop.current.run()
  • CADisplayLink — timer a cada quadro VSync, obrigatório para animações suaves; adicionar em .common para funcionar durante o scroll
  • RunLoop Observer — monitoramento de fases: Entry, BeforeTimers, BeforeSources, AfterWaiting, Exit; usado para perfilamento
  • RunLoop ≠ Looper: o RunLoop do iOS suporta modos e timers, o Looper do Android é mais simples — sem modos, Handler + MessageQueue

Vamos desenvolver um aplicativo móvel chave na mão

A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.

Discutir o projeto

Leia também