Procesamiento asincrónico en el desarrollo de Android e iOS (1) -apertura (lanzamiento del código fuente de GitHub)


Prefacio e introducción


Con respecto al tema " Procesamiento asincrónico en el desarrollo de Android e iOS ", comencé a pensar en él en el primer semestre de este año y ahora he completado tres artículos (el plan original era un total de siete artículos). Hasta ahora he estado buscando una forma de expresión más adecuada y no la he publicado oficialmente.


En los últimos días, he organizado todos los códigos relevantes en GitHub (https://github.com/tielei/AsyncProgrammingDemos). El código es principalmente código de Android (el código de iOS se agregará más adelante).


En este proceso de revisión constante, he sentido cada vez más que la "programación asincrónica" es un tema muy importante, y puede ser mi mayor ganancia en programación móvil en los últimos años. De hecho, los problemas asincrónicos siempre han sido un tema importante en algunos sistemas distribuidos.Muchos protocolos distribuidos se han inventado para hacer frente a los desafíos causados ​​por eventos asincrónicos (tal vez podamos charlar juntos en el futuro). Este problema se limita al entorno de un solo proceso de desarrollo del cliente, tiene sus características especiales y es digno de nuestra conclusión y reflexión.


Los tres artículos se han agrupado y se estima que llevará mucho tiempo leerlos todos. Cada artículo trata un aspecto del tema, pero básicamente son independientes entre sí. También puede elegir el que le interese leer. Dado que los tres artículos están agrupados, es imposible agregar referencias entre sí en el artículo. Los estudiantes que no hayan recibido los tres artículos pueden enviar la palabra "asincrónico" a la cuenta oficial (Zhang Tielei), y todo el contenido relevante se enviará de una vez. Darte.


- 2016.08.17


El siguiente es el texto, bienvenido a leer.




Este artículo es el comienzo de una serie de "Procesamiento asincrónico en el desarrollo de Android e iOS" que pretendo completar.


Desde 2012, comenzamos a desarrollar la primera versión iOS de la aplicación Wei Ai , y he estado en contacto con el desarrollo de iOS y Android para todo el equipo durante 4 años. Mirando hacia atrás ahora para resumir, ¿cuáles son las características únicas del desarrollo de iOS y Android en comparación con el desarrollo en otros campos? ¿Qué habilidades debe poseer un desarrollador calificado de iOS o Android?


Si distingue cuidadosamente, el desarrollo de los clientes iOS y Android aún se puede dividir en dos partes: "front-end" y "back-end" (al igual que el desarrollo de servidores se puede dividir en "front-end" y "back-end").


El llamado trabajo "front-end" es la parte que está más relacionada con la interfaz de UI, como ensamblar páginas, implementar interacción, reproducir animación, desarrollar controles personalizados, etc. Obviamente, para poder completar esta parte del trabajo con facilidad, los desarrolladores deben tener un conocimiento profundo de la tecnología "front-end" relacionada con el sistema, que incluye principalmente tres partes:

  • Renderizado (para resolver el problema del contenido de visualización)

  • diseño (resuelva el problema del tamaño y la posición de la pantalla)

  • Manejo de eventos (resolver problemas interactivos)


El trabajo de "back-end" es algo oculto detrás de la interfaz de usuario. Por ejemplo, manipulación y organización de datos, mecanismo de almacenamiento en caché, cola de envío, diseño y gestión del ciclo de vida, programación de redes, push y monitoreo, etc. Esta parte del trabajo, en el análisis final, se ocupa de cuestiones "lógicas" y no son exclusivas de los sistemas iOS o Android. Sin embargo, existe una gran categoría de problemas que ocupa una gran proporción en la programación "back-end", y así es como "procesar asincrónicamente" "tareas asincrónicas".


En particular, vale la pena señalar que la mayoría de los desarrolladores de clientes, su formación, experiencia de aprendizaje y experiencia de desarrollo, parecen estar más centrados en la parte "front-end", mientras que existen ciertas lagunas en la parte de programación "back-end". Por lo tanto, este artículo intentará resumir los problemas del "procesamiento asincrónico" estrechamente relacionados con la programación "back-end".


Este artículo es el primero de una serie de artículos "Procesamiento asincrónico en el desarrollo de Android e iOS" A primera vista, parece que el tema no es demasiado grande, pero es muy importante. Por supuesto, si pretendo enfatizar su importancia en la programación del cliente, también puedo decir: Mirando todo el proceso de programación del cliente, no es más que "procesamiento asincrónico" de varias "tareas asincrónicas", al menos, Por la parte que no tiene nada que ver con las características del sistema, no tengo grandes problemas con esto.


Entonces, ¿qué se entiende aquí por "procesamiento asincrónico"?


En nuestra programación, a menudo necesitamos realizar algunas tareas asincrónicas. Después de que se inician estas tareas, la persona que llama puede continuar haciendo otras cosas sin esperar a que se ejecute la tarea, y cuando se ejecuta la tarea es incierta e impredecible. Este artículo discutirá todos los aspectos que pueden estar involucrados en el proceso de manejo de estas tareas asincrónicas.


Para que el contenido a discutir sea más claro, se enumera un esquema de la siguiente manera:

  • (1) Descripción general: presente tareas asincrónicas comunes y por qué este tema es tan importante.

  • (2) Devolución de llamada de tarea asincrónica: discute una serie de temas relacionados con la interfaz de devolución de llamada, como el manejo de errores, el modelo de hilo, los parámetros de transmisión transparente, la secuencia de devolución de llamada, etc.

  • (3) Realizar múltiples tareas asincrónicas

  • (4) Tareas y colas asincrónicas

  • (5) Cancelación y suspensión de tareas asincrónicas, e ID de inicio. Cancelar la tarea asincrónica que se está ejecutando es realmente muy difícil.

  • (6) Acerca del bloqueo y desbloqueo de pantalla

  • (7) Análisis de instancias de Android Service: Android Service proporciona un marco riguroso para ejecutar tareas asincrónicas (es posible que se proporcionen otros análisis de instancias más adelante y se agreguen a esta serie).


Obviamente, lo que este artículo discutirá es la primera parte del esquema.


Para describirlo claramente, el código que aparece en esta serie de artículos se ha organizado en GitHub (actualizado continuamente), la dirección base del código es:


  • https://github.com/tielei/AsyncProgrammingDemos


Entre ellos, el código Java que aparece en el artículo actual se encuentra en el paquete com.zhangtielei.demos.async.programming.introduction; y el código de iOS se encuentra en un directorio separado de iOSDemos.


A continuación se muestran dos capturas de pantalla de la aplicación de Android generadas a partir de este código fuente:




A continuación, comenzamos con un pequeño ejemplo específico: Service Binding en Android.




El ejemplo anterior muestra un uso típico de la interacción entre Actividad y Servicio. La actividad está vinculada al servicio cuando está en Reanudar y no está vinculada al Servicio cuando está en pausa. Una vez que la vinculación es exitosa, se llama a onServiceConnected. En este momento, la Actividad obtiene la instancia de IBinder entrante (parámetro de servicio) y puede comunicarse con el Servicio (en proceso o entre procesos) a través de llamadas a métodos. Por ejemplo, las operaciones que se realizan con frecuencia en onServiceConnected en este momento pueden incluir: grabar IBinder y almacenarlo en las variables miembro de Activity para llamadas posteriores; llamar a IBinder para obtener el estado actual del Servicio; establecer métodos de devolución de llamada para monitorear cambios de eventos posteriores del Servicio ; Espere, y así sucesivamente.


Este proceso parece impecable en la superficie. Sin embargo, si considera que bindService es una llamada "asincrónica", habrá una laguna lógica en el código anterior. En otras palabras, bindService se llama solo equivalente a iniciar el proceso de enlace, no espera el final del proceso de enlace antes de regresar. Cuando finaliza el proceso de vinculación (es decir, se llama a onServiceConnected) es impredecible, dependiendo de la velocidad del proceso de vinculación. Según el ciclo de vida de la Actividad, después de onResume, onPause se ejecutará en cualquier momento. De esta manera, después de ejecutar bindService, onServiceConnected puede ejecutarse antes que onPause, o onPause puede ejecutarse antes que onServiceConnected.


Por supuesto, en general, onPause no se ejecutará tan rápido, por lo que onServiceConnected generalmente se ejecutará antes de onPause. Sin embargo, desde una perspectiva "lógica", no podemos ignorar completamente otra posibilidad. De hecho, es realmente posible que suceda, como volver al fondo en cuanto se abre la página, esta posibilidad puede ocurrir con una probabilidad muy pequeña. Una vez que esto suceda, el último onServiceConnected ejecutado establecerá la relación de referencia y monitoreo entre Actividad y Servicio. En este momento, es probable que la aplicación esté en segundo plano, pero Activity e IBinder pueden seguir refiriéndose entre sí. Esto puede provocar que los objetos Java no se publiquen durante mucho tiempo y otros problemas extraños.


Aquí hay un detalle más: el rendimiento final en realidad depende de la implementación interna del unbindService del sistema. Cuando onPause se ejecuta antes que onServiceConnected, onPause llama primero a unbindService. Si unbindService puede garantizar estrictamente que la devolución de llamada de ServiceConnection ya no ocurrirá después de la llamada, eventualmente no hará que la Actividad mencionada y IBinder se refieran entre sí. Sin embargo, unbindService no parece tener tales garantías externas y, según la experiencia personal, en diferentes versiones del sistema Android, unbindService se comporta de manera diferente en este punto.


Al igual que el análisis anterior, siempre que comprendamos todas las situaciones posibles que pueden desencadenarse por la tarea asincrónica bindService, no es difícil encontrar contramedidas similares a las siguientes.


imagen


Veamos un pequeño ejemplo de iOS.


Ahora supongamos que queremos mantener una conexión TCP larga desde el cliente al servidor. Esta conexión se puede volver a conectar automáticamente cuando cambia el estado de la red. Primero, necesitamos una clase que pueda monitorear los cambios de estado de la red. Esta clase se llama Accesibilidad y su código es el siguiente:




El código anterior encapsula la interfaz de la clase Reachability. Cuando la persona que llama quiere iniciar el monitoreo del estado de la red, llama a startNetworkMonitoring; cuando se completa el monitoreo, llama a stopNetworkMonitoring. La larga conexión que imaginamos solo necesita crear y llamar objetos de alcance para manejar los cambios de estado de la red. La parte relevante de su código puede tener el siguiente aspecto (nombre de clase ServerConnection; código de archivo de encabezado ignorado):





La conexión persistente ServerConnection crea una instancia de Accesibilidad cuando se inicializa e inicia la supervisión (llamada startNetworkMonitoring) y establece el método de supervisión (networkStateChanged :) a través de la transmisión del sistema; cuando se destruye la conexión persistente ServerConnection (dealloc), detiene la supervisión (llama a stopNetworkMonitoring).


Cuando cambie el estado de la red, se llamará a networkStateChanged: y se pasará el estado actual de la red. Si se encuentra que la red está disponible (no en el estado NotReachable), la operación de reconexión se realiza de forma asincrónica.


Este proceso parece razonable. Pero esconde un problema fatal.


Durante la operación de reconexión, usamos dispatch_async para iniciar una tarea asincrónica. Cuando esta tarea asincrónica se ejecuta después de iniciarse, es impredecible, según la rapidez con la que se realice la operación de reconexión. Suponiendo que la ejecución de la reconexión es lenta (para operaciones que involucran la red, esto es muy probable), entonces puede ocurrir una situación: la reconexión aún se está ejecutando, pero ServerConnection está a punto de destruirse. Es decir, todos los demás objetos en todo el sistema han publicado referencias a ServerConnection, dejando solo una referencia a uno mismo por bloque durante la programación dispatch_async.


¿Cuales son las consecuencias de esto?


Esto conducirá a: cuando se ejecuta la reconexión, ServerConnection realmente se libera y su método dealloc no se ejecuta en el hilo principal. Se ejecuta en socketQueue.


¿Y qué pasa después? Depende de la realización de Accesibilidad.


Volvamos a analizar el código de Accesibilidad para obtener el impacto final de este evento. Cuando esto sucede, se llama a stopNetworkMonitoring de Reachability en un hilo no principal. Pero cuando se llamó a startNetworkMonitoring, estaba en el hilo principal. Ahora hemos visto que si startNetworkMonitoring y stopNetworkMonitoring no se ejecutan en el mismo hilo antes y después, entonces CFRunLoopGetCurrent () en su implementación no se refiere al mismo Run Loop. Este "error" se ha producido lógicamente. Después de que ocurriera este "error", SCNetworkReachabilityUnscheduleFromRunLoop en stopNetworkMonitoring no pudo descargar la instancia de Reachability del Run Loop originalmente programado en el hilo principal. En otras palabras, si el estado de la red cambia de nuevo después, el ReachabilityCallback aún se ejecutará, pero la instancia de Reachability original ya ha sido destruida (por la destrucción de ServerConnection). De acuerdo con la implementación actual del código anterior, en este momento el parámetro info en ReachabilityCallback apunta a un objeto Reachability que se ha lanzado, por lo que no es sorprendente que ocurra un bloqueo a continuación.


Alguien puede decir que el bloque ejecutado por dispatch_async no debe hacer referencia directa a sí mismo, sino que debe usar baile débil-fuerte. Es decir, cambie el código dispatch_async a la siguiente forma:

__weak ServerConnection *wself = self;        
dispatch_async(socketQueue, ^{    __strong ServerConnection *sself = wself;    [sself reconnect]; });

¿Tiene algún efecto este cambio? Según nuestro análisis anterior, obviamente no. El dealloc de ServerConnection aún se ejecuta en un hilo no principal y los problemas anteriores aún existen. La danza débil-fuerte está diseñada para resolver el problema de las referencias circulares, pero no puede resolver el problema del retraso de tareas asincrónicas que encontramos aquí.


De hecho, incluso si se cambia a la siguiente forma, todavía no tiene ningún efecto.

__weak ServerConnection *wself = self;
dispatch_async(socketQueue, ^{    [wself reconnect]; });

即使拿weak引用(wself)来调用reconnect方法,它一旦执行,也会造成ServerConnection的引用计数增加。结果仍然是dealloc在非主线程上执行。


那既然dealloc在非主线程上执行会造成问题,那我们强制把dealloc里面的代码调度到主线程执行好了,如下:

- (void)dealloc {    
   dispatch_async(dispatch_get_main_queue(), ^{        [reachability stopNetworkMonitoring];    });    [[NSNotificationCenter defaultCenter] removeObserver:self]; }

显然,在dealloc再调用dispatch_async的这种方法也是行不通的。因为在dealloc执行过之后,ServerConnection实例已经被销毁了,那么当block执行时,reachability就依赖了一个已经被销毁的ServerConnection实例。结果还是崩溃。


那不用dispatch_async好了,改用dispatch_sync好了。仔细修改后的代码如下:

- (void)dealloc {    
   if (![NSThread isMainThread]) {        
       dispatch_sync(dispatch_get_main_queue(), ^{            [reachability stopNetworkMonitoring];        });    }    
   else {        [reachability stopNetworkMonitoring];    }    [[NSNotificationCenter defaultCenter] removeObserver:self]; }

经过“前后左右”打补丁,我们现在总算得到了一段可以基本能正常执行的代码了。然而,在dealloc里执行dispatch_sync这种可能耗时的“同步”操作,总不免令人胆战心惊。


那到底怎样做更好呢?


个人认为:并不是所有的销毁工作都适合写在dealloc里


dealloc最擅长的事,自然还是释放内存,比如调用各个成员变量的release(在ARC中这个release也省了)。但是,如果要依赖dealloc来维护一些作用域更广(超出当前对象的生命周期)的变量或过程,则不是一个好的做法。原因至少有两点:

  • dealloc的执行可能会被延迟,无法确保精确的执行时间;

  • 无法控制dealloc是否会在主线程被调用。


比如上面的ServerConnection的例子,业务逻辑自己肯定知道应该在什么时机去停止监听网络状态,而不应该依赖dealloc来完成它。


另外,对于dealloc可能会在异步线程执行的问题,我们应该特别关注它。对于不同类型的对象,我们应该采取不同的态度。比如,对于起到View角色的对象,我们的正确态度是:不应该允许dealloc在异步线程执行的情况出现。为了避免出现这种情况,我们应该竭力避免在View里面直接启动异步任务,或者避免在生命周期更长的异步任务中对View产生强引用。


在上面两个例子中,问题出现的根源在于异步任务。我们仔细思考后会发现,在讨论异步任务的时候,我们必须关注一个至关重要的问题,即条件失效问题。当然,这也是一个显而易见的问题:当一个异步任务真正执行的时候(或者一个异步事件真正发生的时候),境况很可能已与当初调度它时不同,或者说,它当初赖以执行或发生的条件可能已经失效。


在第一个Service Binding的例子中,异步绑定过程开始调度的时候(bindService被调用的时候),Activity还处于Running状态(在执行onResume);而绑定过程结束的时候(onServiceConnected被调用的时候),Activity却已经从Running状态中退出(执行过了onPause,已经又解除绑定了)。


在第二个网络监听的例子中,当异步重连任务结束的时候,外部对于ServerConnection实例的引用已经不复存在,实例马上就要进行销毁过程了。继而造成停止监听时的Run Loop也不再是原来那一个了。


在开始下一节有关异步任务的正式讨论之前,我们有必要对iOS和Android中经常碰到的异步任务做一个总结。

  1. 网络请求。由于网络请求耗时较长,通常网络请求接口都是异步的(例如iOS的NSURLConnection,或Android的Volley)。一般情况下,我们在主线程启动一个网络请求,然后被动地等待请求成功或者失败的回调发生(意味着这个异步任务的结束),最后根据回调结果更新UI。从启动网络请求,到获知明确的请求结果(成功或失败),时间是不确定的。

  2. 通过线程池机制主动创建的异步任务。对于那些需要较长时间同步执行的任务(比如读取磁盘文件这种延迟高的操作,或者执行大计算量的任务),我们通常依靠系统提供的线程池机制把这些任务调度到异步线程去执行,以节约主线程宝贵的计算时间。关于这些线程池机制,在iOS中,我们有GCD(dispatch_async)、NSOperationQueue;在Android上,我们有JDK提供的传统的ExecutorService,也有Android SDK提供的AsyncTask。不管是哪种实现形式,我们都为自己创造了大量的异步任务。

  3. Run Loop调度任务。在iOS上,我们可以调用NSObject的若干个performSelectorXXX方法将任务调度到目标线程的Run Loop上去异步执行(performSelectorInBackground:withObject:除外)。类似地,在Android上,我们可以调用Handler的post/sendMessage方法或者View的post方法将任务异步调度到对应的Run Loop上去。实际上,不管是iOS还是Android系统,一般客户端的基础架构中都会为主线程创建一个Run Loop(当然,非主线程也可以创建Run Loop)。它可以让长时间存活的线程周期性地处理短任务,而在没有任务可执行的时候进入睡眠,既能高效及时地响应事件处理,又不会耗费多余的CPU时间。同时,更重要的一点是,Run Loop模式让客户端的多线程编程逻辑变得简单。客户端编程比服务器编程的多线程模型要简单,很大程度上要归功于Run Loop的存在。在客户端编程中,当我们想执行一个长的同步任务时,一般先通过前面(2)中提及的线程池机制将它调度到异步线程,在任务执行完后,再通过本节提到的Run Loop调度方法或者GCD等机制重新调度回主线程的Run Loop上。这种“主线程->异步线程->主线程”的模式,基本成为了客户端多线程编程的基本模式。这种模式规避了多个线程之间可能存在的复杂的同步操作,使处理变得简单。在后面第(三)部分——执行多个异步任务,我们还有机会继续探讨这个话题。

  4. Retrasar la programación de tareas. Este tipo de tarea comienza a ejecutarse después de un período de tiempo específico o en un momento específico, y se puede utilizar para implementar una estructura similar a una cola de reintentos. Hay muchas formas de implementar tareas de programación retrasadas. En iOS, performSelector: withObject: afterDelay: of NSObject, dispatch_after o dispatch_time de GCD, y NSTimer; en Android, postDelayed y postAtTime de Handler, postDelayed de View y java.util.Timer anticuado, además , Android también tiene un programador-AlarmService más pesado que puede activar automáticamente el programa cuando se ejecuta la programación de tareas.

  5. Comportamiento asincrónico relacionado con la implementación del sistema. Hay muchos tipos de comportamientos de este tipo, aquí hay algunos ejemplos. Por ejemplo: startActivity en Android es una operación asincrónica y todavía hay un breve período de tiempo después de que se llama a la llamada hasta que se crea y se muestra la actividad. Otro ejemplo: el ciclo de vida de la actividad y el fragmento es asincrónico, incluso si el ciclo de vida de la actividad ha llegado al reanudar, todavía no sabe dónde ha ido el ciclo de vida del fragmento que contiene (y si se ha creado su nivel de vista ). Por otro ejemplo, existen mecanismos para monitorear los cambios de estado de la red en los sistemas iOS y Android (esto está involucrado en el segundo ejemplo de código anteriormente en este artículo) Cuando se ejecuta el cambio de estado de la red, la devolución de llamada es un evento asincrónico. Estos comportamientos asincrónicos también requieren un procesamiento asincrónico unificado y completo.


Este artículo también debe aclarar una pregunta sobre el tema al final. Aunque esta serie se denomina "Procesamiento asincrónico en el desarrollo de Android e iOS", el tema del procesamiento de tareas asincrónico no se limita en la práctica al "Desarrollo de iOS o Android". Por ejemplo, también se puede encontrar en el desarrollo de servidores. de. Lo que quiero expresar en esta serie es más una lógica abstracta, no limitada a una tecnología específica de iOS o Android. Sin embargo, en el desarrollo front-end de iOS y Android, las tareas asincrónicas se utilizan tanto que deberíamos tratarlo como un problema más general.




Supongo que te gusta

Origin blog.51cto.com/15049790/2562663
Recomendado
Clasificación