ios - optimizar - mi iphone esta lento
¿Cómo mejorar el rendimiento de UCollectionView que contiene muchas imágenes pequeñas? (7)
- No es necesario obtener una imagen del directorio del documento cada vez que aparece la celda.
- Una vez que obtenga la imagen, puede guardarla en NSCache, la próxima vez solo tendrá que obtener una imagen de NSCache en lugar de recuperarla del directorio de documentos.
- Crear un objeto para NSCache objCache;
En su cellForItemAtIndexPath, simplemente escriba
UIImage *cachedImage = [objCache objectForKey:arr_PathFromDocumentDirectory[indexPath.row]]; if (cachedImage) { imgView_Cell.image = cachedImage; } else { dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); dispatch_async(q, ^{ /* Fetch the image from the Document directory... */ [self downloadImageWithURL:arr_PathFromDocument[indexPath.row] completionBlock:^(BOOL succeeded, CGImageRef data, NSString* path) { if (succeeded) { dispatch_async(dispatch_get_main_queue(), ^{ UIImage *img =[UIImage imageWithCGImage:data]; imgView_Cell.image = img; [objCache setObject:img forKey::arr_PathFromDocument[indexPath.row]]; }); } }]; }); }
- Una vez que obtenga la imagen, configúrela en NSCache con ruta. La próxima vez que compruebe si ya se ha descargado, configúrelo solo desde la memoria caché.
Si necesita ayuda, por favor hágamelo saber.
¡Gracias!
En mi aplicación iOS tengo UICollectionView
que muestra alrededor de 1200 imágenes pequeñas (35x35 puntos). Las imágenes se almacenan en el paquete de la aplicación.
Estoy reutilizando correctamente los UICollectionViewCell
s pero todavía tengo problemas de rendimiento que varían según la forma en que UICollectionViewCell
la carga de imágenes:
Mi aplicación es la extensión de la aplicación y las que tienen memoria limitada (40 MB en este caso). Al colocar todas las 1200 imágenes en el catálogo de Activos y cargarlas con
UIImage(named: "imageName")
produjeron bloqueos de memoria: el sistema almacenó imágenes en caché que llenaron la memoria. En algún momento, la aplicación necesita asignar porciones más grandes de memoria, pero estas no estaban disponibles debido a las imágenes en caché. En lugar de activar la advertencia de memoria y limpiar el caché, el sistema operativo acaba con la aplicación.Cambié el enfoque para evitar el almacenamiento en caché de imágenes. Pongo imágenes en mi proyecto (no en el catálogo de conjuntos) como archivos png y las estoy cargando usando
NSBundle.mainBundle().pathForResource("imageName", ofType: "png")
ahora. La aplicación ya no se bloquea debido a un error de memoria, pero la carga de una sola imagen lleva mucho más tiempo y el desplazamiento rápido se está retrasando incluso en los iPhones más nuevos.
Tengo control total sobre las imágenes y puedo transformarlas, por ejemplo, en .jpeg u optimizarlas (ya probé ImageOptim y algunas otras opciones sin éxito).
¿Cómo puedo resolver ambos problemas de rendimiento a la vez?
EDITAR 1:
También intenté cargar imágenes en hilo de fondo. Este es el código de mi subclase de UICollectionViewCell
:
private func loadImageNamed(name: String) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { [weak self] () in
let image = bundle.pathForResource(name, ofType: "png")?.CGImage
if name == self?.displayedImageName {
dispatch_async(dispatch_get_main_queue(), {
if name == self?.displayedImageName {
self?.contentView.layer.contents = image
}
})
}
})
}
Esto hace que el desplazamiento sea suave sin consumir memoria adicional para el almacenamiento en caché, pero cuando se desplaza a alguna ubicación programáticamente (por ejemplo, cuando UICollectionView
desplaza hacia arriba) causa otro problema : durante la animación de desplazamiento las imágenes no se actualizan (el desplazamiento es demasiado rápido para que se carguen) y Una vez finalizado el desplazamiento, las imágenes incorrectas se muestran durante una fracción de segundo, y una tras otra se reemplazan por las correctas. Esto es muy perturbador visualmente.
EDIT 2:
No puedo agrupar imágenes pequeñas en imágenes compuestas más grandes y mostrarlas como lo sugiere esta respuesta .
Razones:
- Considere diferentes tamaños de pantalla y orientaciones. Debería haber imágenes precompuestas para cada una de ellas, lo que haría que la descarga de la aplicación fuera enorme.
- Las imágenes pequeñas pueden mostrarse en un orden diferente, algunas de ellas pueden estar ocultas en alguna situación. Seguramente no puedo tener imágenes precompuestas para cada combinación y orden posible.
Debe crear una cola para cargar la imagen de forma asíncrona. La mejor opción es la última en salir de la cola. Puedes echar un vistazo a este LIFOOperationQueue . Una cosa importante es evitar mostrar imágenes erróneas que deban manejarse por separado. Para hacerlo, cuando cree una operación para cargar la imagen, asígnele la indexPath actual como identificador. Y luego, en la función de devolución de llamada, verifique si el indexPath dado es visible para actualizar la vista
if (self.tableView.visibleIndexPath().containsObject(indexPath) {
cell.imageView.image = img;
}
También debe tener que personalizar LIFOOperationQueue para tener el número máximo de tareas en cola para que pueda eliminar tareas innecesarias. Es bueno establecer que el número máximo de tareas sea 1.5 * numberOfVisibleCell
.
Una última cosa, debe crear una operación de carga de imagen en willDisplayCell
en lugar de cellForRowAtIndexPath
El cambio de PNG a JPEG no ayudará a guardar la memoria, porque cuando carga una imagen de un archivo a la memoria, se extrae de los datos comprimidos a los bytes no comprimidos.
Y para el problema de rendimiento, recomendaría que cargue la imagen de forma asíncrona y actualice la vista mediante delegado / bloque. Y guarde algunas imágenes en la memoria (pero no todas, digamos 100)
¡Espero que esto ayude!
Para resolver los problemas que se describen en EDIT 1
, debe anular el método prepareForReuse
en la subclase UICollectionViewCell
y restablecer el contenido de su capa a nil:
- (void) prepareForReuse
{
self.contentView.layer.contents = nil;
}
Para cargar tu imagen en segundo plano puedes usar la siguiente:
- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath
{
NSString* imagePath = #image_path#;
weakself;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//load image
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
//redraw image using device context
UIGraphicsBeginImageContextWithOptions(image.size, YES, 0);
[image drawAtPoint:CGPointZero];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
strongself;
if ([[self.collectionView indexPathsForVisibleItems] indexOfObject:indexPath] != NSNotFound) {
cell.contentView.layer.contents = (__bridge id)(image.CGImage);
} else {
// cache image via NSCache with countLimit or custom approach
}
});
});
}
Cómo mejorar:
- Use
NSCache
o el algoritmo de caché personalizado para no cargar imágenes mientras se desplaza todo el tiempo. - Utilice el formato de archivo adecuado. La descompresión JPEG funciona más rápido para imágenes como fotografías. Use PNG para imágenes con áreas planas de color, líneas nítidas, etc.
Puedo proponer una forma alternativa que probablemente pueda resolver su problema:
Considere la posibilidad de hacer bloques de imágenes a imágenes compuestas individuales. Una imagen tan grande debería cubrir el tamaño de la ventana de aplicación. Para el usuario se verá como una colección de imágenes pequeñas, pero técnicamente será una tabla de imágenes grandes.
Su diseño actual:
| | | |
| cell | cell | cell | -> cells outside of screen
| | | |
************************
*| | | |*
*| cell | cell | cell |* -> cells displayed on screen
*| | | |*
*----------------------*
*| | | |*
*| cell | cell | cell |* -> cells displayed on screen
*| | | |*
*----------------------*
*| | | |*
*| cell | cell | cell |* -> cells displayed on screen
*| | | |*
************************
| | | |
| cell | cell | cell | -> cells outside of screen
| | | |
Diseño propuesto:
| |
| cell with |
| composed image | -> cell outside of screen
| |
************************
*| |*
*| |*
*| |*
*| |*
*| cell with |*
*| composed image |* -> cell displayed on screen
*| |*
*| |*
*| |*
*| |*
*| |*
************************
| |
| cell with |
| composed image | -> cell outside of screen
| |
Idealmente, si renderizas previamente dichas imágenes compuestas y las pones para proyectar en el momento de la construcción, pero también puedes renderizarlas en tiempo de ejecución. Seguro que la primera variante trabajará mucho más rápido. Pero en cualquier caso, una sola imagen grande cuesta menos memoria que las partes separadas de esa imagen.
Si tiene la posibilidad de pre-renderizarlos, use el formato JPEG. En este caso, probablemente su primera solución (cargar imágenes con [UIImage imageNamed:]
en el hilo principal) funcionará bien porque se usa menos memoria, el diseño es mucho más simple.
Si tiene que renderizarlos en tiempo de ejecución, entonces necesitará usar su solución actual (trabaje en segundo plano), y seguirá viendo los errores de ubicación de la imagen cuando ocurra una animación rápida, pero en este caso será un error de ubicación único (una imagen cubre la ventana marco), por lo que debe verse mejor.
Si necesita saber qué imagen (imagen pequeña original 35x35) hizo clic en el usuario, puede usar UITapGestureRecognizer
adjunto a la celda. Cuando se reconoce el gesto, puede usar el método locationInView:
para calcular el índice correcto de la imagen pequeña.
No puedo decir que resuelva el problema al 100%, pero tiene sentido intentarlo.
Revisa este tutorial:
http://www.raywenderlich.com/86365/asyncdisplaykit-tutorial-achieving-60-fps-scrolling
Utiliza AsyncDisplayKit para cargar imágenes en un hilo de fondo.
Un par de cosas que puedes hacer para esto,
- Debe cargar solo aquellas imágenes en las que se vuelven a adquirir y descargarlas cuando no se vuelva a adquirir, esto hará que su aplicación use menos memoria.
Otra cosa cuando carga una imagen en segundo plano, decodifíquela en un hilo de fondo antes de configurarla en la vista de imagen ... la imagen predeterminada se decodificará cuando se establezca y eso ocurrirá generalmente en el hilo principal, esto hará que el desplazamiento sea suave, puede decodificar una imagen por el código de abajo.
static UIImage *decodedImageFromData:(NSData *data, BOOL isJPEG) { // Create dataProider object from provided data CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data); // Create CGImageRef from dataProviderObject CGImageRef newImage = (isJPEG) ? CGImageCreateWithJPEGDataProvider(dataProvider, NULL, NO, kCGRenderingIntentDefault) : CGImageCreateWithPNGDataProvider(dataProvider, NULL, NO, kCGRenderingIntentDefault); // Get width and height info from image const size_t width = CGImageGetWidth(newImage); const size_t height = CGImageGetHeight(newImage); // Create colorspace const CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); // Create context object, provide data ref if wean to store other wise NULL // Set width and height of image // Number of bits per comoponent // Number of bytes per row // set color space // And tell what CGBitMapInfo const CGContextRef context = CGBitmapContextCreate( NULL,width, height,8, width * 4, colorspace, (CGBitmapInfo)kCGImageAlphaPremultipliedLast); //Now we can draw image CGContextDrawImage(context, CGRectMake(0, 0, width, height), newImage); // Get CGImage from drwan context CGImageRef drawnImage = CGBitmapContextCreateImage(context); CGContextRelease(context); CGColorSpaceRelease(colorspace); // Create UIImage from CGImage UIImage *image = [UIImage imageWithCGImage:drawnImage]; CGDataProviderRelease(dataProvider); CGImageRelease(newImage); CGImageRelease(drawnImage); return image;
}