O que significa suspender a função em Kotlin Coroutine

118

Estou lendo Kotlin Coroutine e sei que é baseado em suspendfunções. Mas o que isso suspendsignifica?

A co-rotina ou função é suspensa?

De https://kotlinlang.org/docs/reference/coroutines.html

Basicamente, co-rotinas são cálculos que podem ser suspensos sem bloquear um thread

Já ouvi muitas pessoas dizerem "suspender função". Mas acho que é a co-rotina que fica suspensa porque está esperando a função terminar? "suspender" geralmente significa "cessar operação", neste caso a co-rotina está ociosa.

🤔 Devemos dizer que a co-rotina está suspensa?

Qual corrotina é suspensa?

De https://kotlinlang.org/docs/reference/coroutines.html

Para continuar a analogia, await () pode ser uma função de suspensão (portanto, também pode ser chamada de dentro de um bloco {} assíncrono) que suspende uma co-rotina até que algum cálculo seja feito e retorne seu resultado:

async { // Here I call it the outer async coroutine
    ...
    // Here I call computation the inner coroutine
    val result = computation.await()
    ...
}

🤔 Diz "que suspende uma co-rotina até que algum cálculo seja feito", mas a co-rotina é como uma thread leve. Portanto, se a co-rotina for suspensa, como o cálculo pode ser feito?

Vemos que awaité chamado computation, então pode ser asyncque retorne Deferred, o que significa que pode iniciar outra co-rotina

fun computation(): Deferred<Boolean> {
    return async {
        true
    }
}

🤔 A citação diz que suspende uma co-rotina . Significa suspenda asyncco-rotina externa ou suspenda computationco-rotina interna ?

Faz suspendmédia que, enquanto exterior asyncco-rotina está esperando ( await) para o interior computationcoroutine ao fim, ele (o exterior asynccoroutine) idles (daí o nome suspensão) e retornos de rosca para o pool de threads, e quando a criança computationcoroutine acabamentos, ele (o exterior asynccoroutine ) acorda, pega outro thread do pool e continua?

Menciono o tópico por causa de https://kotlinlang.org/docs/tutorials/coroutines-basic-jvm.html

O encadeamento é retornado ao conjunto enquanto a co-rotina está esperando e quando a espera é concluída, a co-rotina é retomada em um encadeamento livre no conjunto

onmyway133
fonte

Respostas:

113

Funções suspensas estão no centro de todas as corrotinas. Uma função de suspensão é simplesmente uma função que pode ser pausada e retomada posteriormente. Eles podem executar uma operação de longa duração e esperar que ela seja concluída sem bloquear.

A sintaxe de uma função de suspensão é semelhante à de uma função regular, exceto pela adição da suspendpalavra - chave. Pode receber um parâmetro e ter um tipo de retorno. No entanto, as funções de suspensão só podem ser chamadas por outra função de suspensão ou dentro de uma co-rotina.

suspend fun backgroundTask(param: Int): Int {
     // long running operation
}

Por dentro, as funções de suspensão são convertidas pelo compilador em outra função sem a palavra-chave suspend, que recebe um parâmetro de adição do tipo Continuation<T>. A função acima, por exemplo, será convertida pelo compilador para esta:

fun backgroundTask(param: Int, callback: Continuation<Int>): Int {
   // long running operation
}

Continuation<T> é uma interface que contém duas funções que são chamadas para retomar a co-rotina com um valor de retorno ou com uma exceção se um erro ocorreu enquanto a função estava suspensa.

interface Continuation<in T> {
   val context: CoroutineContext
   fun resume(value: T)
   fun resumeWithException(exception: Throwable)
}
Sofien Rahmouni
fonte
4
Outro mistério desvendado! Ótimo!
WindRider
16
Eu me pergunto como essa função é realmente pausada? Eles sempre dizem que suspend funpode ser pausado, mas como exatamente?
WindRider
2
@WindRider Significa apenas que a thread atual começa a executar alguma outra co-rotina e voltará a esta mais tarde.
Joffrey
2
Eu descobri o mecanismo "misterioso". Ele pode ser facilmente revelado com a ajuda de Ferramentas> Kotlin> Bytecode> Decompilar btn. Mostra como o chamado "ponto de suspensão" é implementado - via Continuação e assim por diante. Qualquer um pode dar uma olhada por si mesmo.
WindRider
4
@buzaa Aqui está uma palestra de 2017 de Roman Elizarov que explica isso em nível de bytecode.
Marko Topolnik
30

Para entender exatamente o que significa suspender uma co-rotina, sugiro que você leia este código:

import kotlinx.coroutines.Dispatchers.Unconfined
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

var continuation: Continuation<Int>? = null

fun main() = runBlocking {
    launch(Unconfined) {
        val a = a()
        println("Result is $a")
    }
    10.downTo(0).forEach {
        continuation!!.resume(it)
    }
}

suspend fun a(): Int {
    return b()
}

suspend fun b(): Int {
    while (true) {
        val i = suspendCoroutine<Int> { cont -> continuation = cont }
        if (i == 0) {
            return 0
        }
    }
}

O Unconfineddespachante de corrotina elimina a magia do despacho de corrotina e nos permite focar diretamente nas corrotinas vazias.

O código dentro do launchbloco começa a ser executado imediatamente no thread atual, como parte da launchchamada. O que acontece é o seguinte:

  1. Avalie val a = a()
  2. Este acorrenta a b(), alcançando suspendCoroutine.
  3. A função b()executa o bloco passado suspendCoroutinee retorna um COROUTINE_SUSPENDEDvalor especial . Esse valor não é observável por meio do modelo de programação Kotlin, mas é isso que o método Java compilado faz.
  4. A função a(), vendo esse valor de retorno, ela mesma também o retorna.
  5. O launchbloco faz o mesmo e o controle agora retorna para a linha após a launchinvocação:10.downTo(0)...

Observe que, neste ponto, você tem o mesmo efeito como se o código dentro do launchbloco e seu fun maincódigo estivessem sendo executados simultaneamente. Acontece que tudo isso está acontecendo em uma única thread nativa, então o launchbloco é "suspenso".

Agora, dentro do forEachcódigo de loop, o programa lê o continuationque a b()função escreveu e resumescom o valor de 10. resume()é implementado de tal forma que será como se a suspendCoroutinechamada retornasse com o valor que você passou. Então, de repente, você se encontra no meio da execução b(). O valor que você passou resume()é atribuído ie verificado 0. Se não for zero, o while (true)loop continua por dentro b(), alcançando novamente suspendCoroutine, ponto no qual sua resume()chamada retorna, e agora você passa por outra etapa de loop forEach(). Isso continua até que você finalmente reinicie com 0, então a printlninstrução é executada e o programa é concluído.

A análise acima deve dar a você a importante intuição de que "suspender uma co-rotina" significa retornar o controle de volta à launchinvocação mais interna (ou, mais geralmente, construtor de co-rotina ). Se uma co-rotina for suspensa novamente após retomar, a resume()chamada termina e o controle retorna para o chamador de resume().

A presença de um despachante de corrotina torna esse raciocínio menos claro porque a maioria deles envia imediatamente seu código para outro encadeamento. Nesse caso, a história acima acontece naquele outro encadeamento, e o distribuidor de co-rotina também gerencia o continuationobjeto para que possa retomá-lo quando o valor de retorno estiver disponível.

Marko Topolnik
fonte
21

Em primeiro lugar, a melhor fonte para entender esta IMO é a palestra "Deep Dive into Coroutines" de Roman Elizarov.

A co-rotina ou função é suspensa?

Chamar um suspender ing função de suspensão s a co-rotina, ou seja, o segmento atual pode começar a executar um outro co-rotina. Portanto, a co - rotina é considerada suspensa em vez da função.

Na verdade, sites de chamadas de funções suspensas são chamados de "pontos de suspensão" por esse motivo.

Qual corrotina é suspensa?

Vamos analisar seu código e analisar o que acontece:

// 1. this call starts a new coroutine (let's call it C1).
//    If there were code after it, it would be executed concurrently with
//    the body of this async
async {
    ...
    // 2. this is a regular function call
    val deferred = computation()
    // 4. because await() is suspendING, it suspends coroutine C1.
    //    This means that if we had a single thread in our dispatcher, 
    //    it would now be free to go execute C2
    // 7. once C2 completes, C1 is resumed with the result `true` of C2's async
    val result = deferred.await() 
    ...
    // 8. C1 can now keep going in the current thread until it gets 
    //    suspended again (or not)
}

fun computation(): Deferred<Boolean> {
    // 3. this async call starts a second coroutine (C2). Depending on the 
    //    dispatcher you're using, you may have one or more threads.
    // 3.a. If you have multiple threads, the block of this async could be
    //      executed in parallel of C1 in another thread. The control flow 
    //      of the current thread returns to the caller of computation().
    // 3.b. If you have only one thread, the block is sort of "queued" but 
    //      not executed right away, and the control flow returns to the 
    //      caller of computation(). (unless a special dispatcher or 
    //      coroutine start argument is used, but let's keep it simple).
    //    In both cases, we say that this block executes "concurrently"
    //    with C1.
    return async {
        // 5. this may now be executed
        true
        // 6. C2 is now completed, so the thread can go back to executing 
        //    another coroutine (e.g. C1 here)
    }
}

O externo asyncinicia uma co-rotina. Quando ele chama computation(), o interno asyncinicia uma segunda co-rotina. Então, a chamada para await()suspende a execução da co-rotina externa async , até que a execução da co-rotina interna async termine.

Você pode até ver isso com um único thread: o thread executará o asyncinício do externo , em seguida, chamará computation()e alcançará o interno async. Nesse ponto, o corpo do assíncrono interno é ignorado e o encadeamento continua executando o externo asyncaté atingir await(). await()é um "ponto de suspensão", porque awaité uma função de suspensão. Isso significa que a co-rotina externa é suspensa e, assim, a rosca começa a executar a interna. Quando estiver pronto, ele volta para executar o fim do externo async.

Suspender significa que enquanto a co-rotina assíncrona externa está esperando (aguarda) pela co-rotina de computação interna terminar, ela (a co-rotina assíncrona externa) fica inativa (daí o nome suspender) e retorna thread para o pool de threads, e quando a co-rotina de computação filha termina , ela (a co-rotina assíncrona externa) é ativada, pega outro encadeamento do pool e continua?

Sim, exatamente.

A maneira como isso é realmente alcançado é transformando cada função de suspensão em uma máquina de estado, onde cada "estado" corresponde a um ponto de suspensão dentro dessa função de suspensão. Sob o capô, a função pode ser chamada várias vezes, com as informações sobre o ponto de suspensão a partir do qual ela deve começar a ser executada (você realmente deve assistir ao vídeo vinculado para obter mais informações sobre isso).

Joffrey
fonte
3
Ótima resposta, sinto falta desse tipo de explicação realmente básica quando se trata de corrotinas.
bernardo.g
Por que isso não está implementado em nenhuma outra linguagem? Ou eu estou esquecendo de alguma coisa? Estou pensando nessa solução há tanto tempo, fico feliz que Kotlin a tenha, mas não sei por que TS ou Rust têm algo assim
PEZO
As corrotinas de poço @PEZO já existem há muito tempo. Kotlin não os inventou, mas a sintaxe e a biblioteca os fazem brilhar. Go tem goroutines, JavaScript e TypeScript têm promessas. A única diferença está nos detalhes da sintaxe para usá-los. Acho bastante irritante / perturbador para as asyncfunções de JS serem marcadas dessa forma e ainda assim retornar uma Promessa.
Joffrey
Desculpe, meu comentário não foi claro. Refiro-me à palavra-chave suspender. Não é o mesmo que assíncrono.
PEZO
Obrigado por apontar para o vídeo de Roman. Ouro puro.
Denounce'IN
8

Descobri que a melhor maneira de entender suspendé fazer uma analogia entre thispalavra-chave e coroutineContextpropriedade.

As funções do Kotlin podem ser declaradas como locais ou globais. Funções locais magicamente têm acesso à thispalavra - chave, enquanto as globais não.

As funções do Kotlin podem ser declaradas como suspendou de bloqueio. suspendfunções magicamente têm acesso à coroutineContextpropriedade, enquanto funções de bloqueio não.

A questão é: a coroutineContextpropriedade é declarada como uma propriedade "normal" no Kotlin stdlib, mas esta declaração é apenas um esboço para fins de documentação / navegação. Na verdade, coroutineContexté uma propriedade intrínseca embutida, o que significa que a mágica do compilador está ciente dessa propriedade, assim como está ciente das palavras-chave da linguagem.

O que thispalavra-chave faz para funções locais é o que coroutineContextpropriedade faz para suspendfunções: ela dá acesso ao contexto atual de execução.

Então, você precisa suspendobter um acesso à coroutineContextpropriedade - a instância do contexto de co-rotina executado atualmente

Dmitry Kolesnikovich
fonte
5

Queria dar um exemplo simples do conceito de continuação. Isso é o que uma função de suspensão faz, ela pode congelar / suspender e então continuar / reiniciar. Pare de pensar na co-rotina em termos de threads e Semaphore. Pense nisso em termos de continuação e até mesmo ganchos de retorno de chamada.

Para ficar claro, uma co-rotina pode ser pausada usando uma suspendfunção. vamos investigar isso:

No android, podemos fazer isso, por exemplo:

var TAG = "myTAG:"
        fun myMethod() { // function A in image
            viewModelScope.launch(Dispatchers.Default) {
                for (i in 10..15) {
                    if (i == 10) { //on first iteration, we will completely FREEZE this coroutine (just for loop here gets 'suspended`)
                        println("$TAG im a tired coroutine - let someone else print the numbers async. i'll suspend until your done")
                        freezePleaseIAmDoingHeavyWork()
                    } else
                        println("$TAG $i")
                    }
            }

            //this area is not suspended, you can continue doing work
        }


        suspend fun freezePleaseIAmDoingHeavyWork() { // function B in image
            withContext(Dispatchers.Default) {
                async {
                    //pretend this is a big network call
                    for (i in 1..10) {
                        println("$TAG $i")
                        delay(1_000)//delay pauses coroutine, NOT the thread. use  Thread.sleep if you want to pause a thread. 
                    }
                    println("$TAG phwww finished printing those numbers async now im tired, thank you for freezing, you may resume")
                }
            }
        }

O código acima imprime o seguinte:

I: myTAG: my coroutine is frozen but i can carry on to do other things

I: myTAG: im a tired coroutine - let someone else print the numbers async. i'll suspend until your done

I: myTAG: 1
I: myTAG: 2
I: myTAG: 3
I: myTAG: 4
I: myTAG: 5
I: myTAG: 6
I: myTAG: 7
I: myTAG: 8
I: myTAG: 9
I: myTAG: 10

I: myTAG: phwww finished printing those numbers async now im tired, thank you for freezing, you may resume

I: myTAG: 11
I: myTAG: 12
I: myTAG: 13
I: myTAG: 14
I: myTAG: 15

imagine funcionando assim:

insira a descrição da imagem aqui

Portanto, a função atual a partir da qual você iniciou não para, apenas uma co-rotina seria suspensa enquanto continua. O thread não é pausado executando uma função de suspensão.

Acho que este site pode te ajudar a esclarecer as coisas e é minha referência.

Vamos fazer algo legal e congelar nossa função de suspensão no meio de uma iteração. Vamos retomar mais tarde emonResume

Armazene uma variável chamada continuatione vamos carregá-la com o objeto de continuação de co-rotinas para nós:

var continuation: CancellableContinuation<String>? = null

suspend fun freezeHere() = suspendCancellableCoroutine<String> {
            continuation = it
        }

 fun unFreeze() {
            continuation?.resume("im resuming") {}
        }

Agora, vamos retornar à nossa função suspensa e fazê-la congelar no meio da iteração:

 suspend fun freezePleaseIAmDoingHeavyWork() {
        withContext(Dispatchers.Default) {
            async {
                //pretend this is a big network call
                for (i in 1..10) {
                    println("$TAG $i")
                    delay(1_000)
                    if(i == 3)
                        freezeHere() //dead pause, do not go any further
                }
            }
        }
    }

Em seguida, em outro lugar, como onResume (por exemplo):

override fun onResume() {
        super.onResume()
        unFreeze()
    }

E o loop continuará. É muito legal saber que podemos congelar uma função de suspensão a qualquer momento e retomá-la após algum tempo. Você também pode procurar canais

j2emanue
fonte
4

Como já existem muitas respostas boas, gostaria de postar um exemplo mais simples para outros.

Caso de uso runBlocking :

  • myMethod () é suspendfunção
  • runBlocking { }inicia uma co-rotina em forma de bloqueio. É semelhante a como bloqueamos threads normais com Threadclasse e notificamos threads bloqueados após certos eventos.
  • runBlocking { }não bloquear a corrente de execução fio, até que a co-rotina (corpo entre {}) fica concluída

     override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_activity)
        Log.i(TAG,"Outer code started on Thread : " + Thread.currentThread().name);
        runBlocking {
            Log.d(TAG,"Inner code started  on Thread : " + Thread.currentThread().name + " making outer code suspend");
            myMethod();
        }
        Log.i(TAG,"Outer code resumed on Thread : " + Thread.currentThread().name);
    }
    
    private suspend fun myMethod() {
        withContext(Dispatchers.Default) {
        for(i in 1..5) {
            Log.d(TAG,"Inner code i : $i on Thread : " + Thread.currentThread().name);
        }
    }

Isso resulta em:

I/TAG: Outer code started on Thread : main
D/TAG: Inner code started  on Thread : main making outer code suspend
// ---- main thread blocked here, it will wait until coroutine gets completed ----
D/TAG: Inner code i : 1 on Thread : DefaultDispatcher-worker-2
D/TAG: Inner code i : 2 on Thread : DefaultDispatcher-worker-2
D/TAG: Inner code i : 3 on Thread : DefaultDispatcher-worker-2
D/TAG: Inner code i : 4 on Thread : DefaultDispatcher-worker-2
D/TAG: Inner code i : 5 on Thread : DefaultDispatcher-worker-2
// ---- main thread resumes as coroutine is completed ----
I/TAG: Outer code resumed on Thread : main

caso de uso de lançamento :

  • launch { } inicia uma co-rotina simultaneamente.
  • Isso significa que quando especificamos launch, uma co-rotina começa a ser executada no workerthread.
  • O workerthread e o thread externo (a partir do qual chamamos launch { }) são executados simultaneamente. Internamente, a JVM pode executar Preemptive Threading
  • Quando precisamos que várias tarefas sejam executadas em paralelo, podemos usar isso. Existem scopesquais especificam o tempo de vida da co-rotina. Se especificarmos GlobalScope, a co-rotina funcionará até que o tempo de vida do aplicativo termine.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_activity)
        Log.i(TAG,"Outer code started on Thread : " + Thread.currentThread().name);
    
        GlobalScope.launch(Dispatchers.Default) {
            Log.d(TAG,"Inner code started  on Thread : " + Thread.currentThread().name + " making outer code suspend");
            myMethod();
        }
        Log.i(TAG,"Outer code resumed on Thread : " + Thread.currentThread().name);
    }
    
    private suspend fun myMethod() {
        withContext(Dispatchers.Default) {
            for(i in 1..5) {
                Log.d(TAG,"Inner code i : $i on Thread : " + Thread.currentThread().name);
            }
        }
    }

Isso resulta em:

10806-10806/com.example.viewmodelapp I/TAG: Outer code started on Thread : main
10806-10806/com.example.viewmodelapp I/TAG: Outer code resumed on Thread : main
// ---- In this example, main had only 2 lines to execute. So, worker thread logs start only after main thread logs complete
// ---- In some cases, where main has more work to do, the worker thread logs get overlap with main thread logs
10806-10858/com.example.viewmodelapp D/TAG: Inner code started  on Thread : DefaultDispatcher-worker-1 making outer code suspend
10806-10858/com.example.viewmodelapp D/TAG: Inner code i : 1 on Thread : DefaultDispatcher-worker-1
10806-10858/com.example.viewmodelapp D/TAG: Inner code i : 2 on Thread : DefaultDispatcher-worker-1
10806-10858/com.example.viewmodelapp D/TAG: Inner code i : 3 on Thread : DefaultDispatcher-worker-1
10806-10858/com.example.viewmodelapp D/TAG: Inner code i : 4 on Thread : DefaultDispatcher-worker-1
10806-10858/com.example.viewmodelapp D/TAG: Inner code i : 5 on Thread : DefaultDispatcher-worker-1

assíncrono e aguardar caso de uso:

  • Quando temos várias tarefas a fazer e elas dependem da realização de outras, asynce awaitajudariam.
  • Por exemplo, no código a seguir, existem 2funções de suspensão myMethod () e myMethod2 (). myMethod2()deve ser executado somente após a conclusão completa de myMethod() OU myMethod2() depende do resultado de myMethod(), podemos usar asynceawait
  • asyncinicia uma co-rotina em paralelo semelhante a launch. Mas, ele fornece uma maneira de esperar por uma co-rotina antes de iniciar outra co-rotina em paralelo.
  • É assim await(). asyncretorna uma instância de Deffered<T>. Tseria Unitpor padrão. Quando precisamos de esperar por qualquer async's conclusão, precisamos chamada .await()na Deffered<T>instância dessa async. Como no exemplo abaixo, chamamos o innerAsync.await()que implica que a execução seria suspensa até innerAsyncser concluída. Podemos observar o mesmo na saída. O innerAsyncé concluído primeiro, que chama myMethod(). E então async innerAsync2começa a seguir , que chamamyMethod2()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_activity)
        Log.i(TAG,"Outer code started on Thread : " + Thread.currentThread().name);
    
         job = GlobalScope.launch(Dispatchers.Default) {
             innerAsync = async {
                 Log.d(TAG, "Inner code started  on Thread : " + Thread.currentThread().name + " making outer code suspend");
                 myMethod();
             }
             innerAsync.await()
    
             innerAsync2 = async {
                 Log.w(TAG, "Inner code started  on Thread : " + Thread.currentThread().name + " making outer code suspend");
                 myMethod2();
             }
        }
    
        Log.i(TAG,"Outer code resumed on Thread : " + Thread.currentThread().name);
        }
    
    private suspend fun myMethod() {
        withContext(Dispatchers.Default) {
            for(i in 1..5) {
                Log.d(TAG,"Inner code i : $i on Thread : " + Thread.currentThread().name);
            }
        }
    }
    
    private suspend fun myMethod2() {
        withContext(Dispatchers.Default) {
            for(i in 1..10) {
                Log.w(TAG,"Inner code i : $i on Thread : " + Thread.currentThread().name);
            }
        }
    }

Isso resulta em:

11814-11814/? I/TAG: Outer code started on Thread : main
11814-11814/? I/TAG: Outer code resumed on Thread : main
11814-11845/? D/TAG: Inner code started  on Thread : DefaultDispatcher-worker-2 making outer code suspend
11814-11845/? D/TAG: Inner code i : 1 on Thread : DefaultDispatcher-worker-2
11814-11845/? D/TAG: Inner code i : 2 on Thread : DefaultDispatcher-worker-2
11814-11845/? D/TAG: Inner code i : 3 on Thread : DefaultDispatcher-worker-2
11814-11845/? D/TAG: Inner code i : 4 on Thread : DefaultDispatcher-worker-2
11814-11845/? D/TAG: Inner code i : 5 on Thread : DefaultDispatcher-worker-2
// ---- Due to await() call, innerAsync2 will start only after innerAsync gets completed
11814-11848/? W/TAG: Inner code started  on Thread : DefaultDispatcher-worker-4 making outer code suspend
11814-11848/? W/TAG: Inner code i : 1 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 2 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 3 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 4 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 5 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 6 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 7 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 8 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 9 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 10 on Thread : DefaultDispatcher-worker-4
Kushal
fonte