UIImage 从服务器逐步显示

Posted

技术标签:

【中文标题】UIImage 从服务器逐步显示【英文标题】:UIImage to be displayed progressively from server 【发布时间】:2012-02-28 07:56:13 【问题描述】:

我一直在尝试显示来自服务器的大图像,但我必须逐步显示它。

我使用了 UIView 的子类,其中我使用了UIImage 对象,其中我使用了 NSURLConnection 及其委托方法,我也使用了

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;

我在其中附加数据并将其转换为UIImage 对象,并使用UIImagedrawInRect: 方法绘制矩形。

一切正常,但问题是,当在上下文中绘制图像时,在将整个图像绘制到屏幕上之前,我无法单击屏幕上的其他任何位置。

有什么好的解决方案,即使在屏幕上绘制图像,我也可以点击其他任何地方?

任何帮助都将不胜感激。

编辑: 在didReceiveData 中是否有任何有效的方法可以逐渐模糊图像?所以drawInRect 不会花太多时间来画画。或者如果有人有自定义的drawRect 方法,它可以有效地将图像逐步显示为didReceiveData 中接收到的数据。

【问题讨论】:

如何在后台线程中运行进程? 我尝试在后台运行进程,但它崩溃了,因为我们无法在后台线程中在屏幕上绘制图像,它应该在主线程中。 这里的问题是你不断地重绘图像,这阻止了 UI 负责。您可以做的是“过滤”重绘,每 10 或 50 次 didReceiveData 迭代进行一次。 是的,你说得对,在 didReceiveData 中多次绘制图像,会产生问题。有没有其他方法可以逐步绘制图像? 如果您的图像是 jpeg 格式,您是否尝试过使用渐进式 jpeg (faqs.org/faqs/jpeg-faq/part1/section-11.html)?我不知道这是否会有所帮助,但你可以试试。 【参考方案1】:

我曾将 NYXImagesKit 用于类似的事情,在不阻塞主线程的情况下下载图像并逐步显示图像。我写了一个非常快速和肮脏的例子来说明基本的工作原理。我在 UITableview 中加载图像以显示它不会阻塞用户界面(主线程)。您可以在图像加载时滚动表格视图。不要忘记添加正确的框架,有几个。这是 Github 上项目的链接:

https://github.com/HubertK/ProgressiveImageDownload

它真的很容易使用,创建一个 NYXProgressiveImageView 对象,设置 URL,它会在您调用时为您完成所有工作:

loadImageAtURL:

它是 UIImageView 的子类,像魔术一样工作!这是开发者网站的链接:

http://www.cocoaintheshell.com/2012/01/nyximageskit-class-nyxprogressiveimageview/

【讨论】:

【参考方案2】:

我建议以异步方式拉取图像数据,然后应用校正以获得从部分下载的 NSData 到 UIImage 的有效转换:

NSURLRequest *theRequest = [NSURLRequest requestWithURL:
                                           [NSURL URLWithString: imageRequestString]
                                            cachePolicy: NSURLRequestReloadIgnoringCacheData
                                        timeoutInterval: 60.0];

NSURLConnection *theConnection = [[NSURLConnection alloc] initWithRequest: theRequest
                                                                 delegate: self];

if (theConnection)
      receivedData = [[NSMutableData data] retain];

.......

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data 
       [receivedData appendData: data];

       NSInvocationOperation *operation = 
              [[NSInvocationOperation alloc] initWithTarget: self
                                                   selector: @selector(loadPartialImage)
                                                     object: nil];
       [[[NSOperationQueue alloc] init] autorelease] addOperation: operation];
       [operation release];


- (void)loadPartialImage 
       // This is where you would call the function that would "stitch up" your partial
       // data and make it appropriate for use in UIImage's imageWithData
       NSData *validPartialData =
          [self validImageRepresentationFromPartialImageData: receivedData];

       UIImage *partialImage = [UIImage imageWithData: validPartialData];

       [imageView performSelectorOnMainThread: @selector(setImage:)
                                   withObject: partialImage
                                waitUntilDone: NO];



+ (void)connectionDidFinishLoading:(NSURLConnection *)connection 
       [connection release];

           UIImage *fullImage = [UIImage imageWithData: receivedData];

           imageView.image = fullImage;

请注意,我没有提供validImageRepresentationFromPartialImageData 的代码,因为目前我没有明确的具体想法,关于如何实现这样的更正,或者 [UIImage imageWithData:] 实际上不会默认接受部分数据作为输入。如您所见,强制转换和 UIImage 创建将发生在不同的线程上,而主线程只会在更新时显示更新。

如果您收到过于频繁的更新并且它们仍然阻止界面,您可以:

一个。也在不同的线程上发出图像请求。 湾。减少 UIImageView 的更新频率,根据图像的大小,在 10 或 100 次更新中只调用一次 setImage。

【讨论】:

感谢您的回复。是的,我在做“a”。你提到的选项,但我担心有没有更好的方法在屏幕上绘制图像而不阻塞界面。可能是一种更好的方式来绘制图像,这样它就可以花费很少的时间来绘制。【参考方案3】:

我通常使用非常简单的 GCD 模式进行异步图像加载:

    创建一个 GCD 队列,您可以在其中从 Web 服务器加载图像数据 在主队列中设置图像数据

示例:

dispatch_queue_t image_queue = dispatch_queue_create("com.company.app.imageQueue", NULL);
dispatch_queue_t main_queue = dispatch_get_main_queue();

dispatch_async(image_queue, ^
  NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[record imageURLString]];
  dispatch_async(main_queue, ^
    [imageView setImage:[UIImage imageWithData:imageData]];
  );
);

【讨论】:

感谢您的回答,但是在 didReceiveData: 方法中绘制图像时出现问题,我无法单击屏幕上的其他任何位置。并且绘图图像应该在主线程中,因为您的回答表明主队列也将在主线程中,所以它会挂起直到图像没有被绘制,因为 didReceiveData: 会被调用很多次...... @JigneshBrahmkhatri 不能先收集所有图像数据,然后将其绘制到屏幕上吗?我想这样更有效率 @JigneshBrahmkhatri Mhh,好的。也许您可以显示一个微调器,它表示加载进度?我认为这是一种比阻塞 UI 线程更好的方法。 我的第一个要求是从服务器逐步显示图像。 你能给我更多的代码吗?我没有时间从头开始写下你的方法——如果我有你实际使用的控制器会更容易帮助你【参考方案4】:

可能didReceiveData 被调用太频繁了!只需使用NSTimer 并以 1-2 秒的步骤定期更新图像。这应该更有效。

您还可以使用performSelectorInBackground将您的NSData 转换为UIImage; 然后调用performSelectorOnMainThread将图像设置到 UIImage 视图中。所以转换的东西不会阻塞主线程。

【讨论】:

感谢您的回复,正如您所说,我正在使用替代解决方案,但我需要适当的解决方案来完成它,因为,我将调用 40-50 UIImageViews 从服务器加载并逐步显示它。所以这个解决方案也行不通。问题是,在屏幕上绘制图像需要太多时间。 绘图时间过长!?你测量了吗?就像我说的那样减少更新计数。那应该工作,不是吗?最终加载多少视图并不重要,只要您正确地选择其中一个即可。只是不要重绘来自 didReceiveData 的每次更新。 我每 20 次更新重绘,控制来自 didReceiveData,但这是针对一个图像,如果有 100 个图像,那么控制可以随时到来,图像将在主线程中绘制,这会挂起应用程序直到正在完全绘制图像。 好的,我们需要更多详细信息。你在说什么图像尺寸?一次将加载多少个?为什么需要渐进式显示? 我需要渐进式显示以确保用户正在加载图像,这是我不能离开这个要求的第一个要求。关于图像,它们来自服务器,可以是 10 或 50【参考方案5】:

您是否考虑过在服务器上将图像分割成更小的块,然后在收到完整的块时重新绘制?这将使您可以通过更改块大小来控制负载的“渐进性”和重绘频率。不过,不确定这是您所追求的渐进式负载。

【讨论】:

【参考方案6】:

如果您可以控制服务器,请将图像分割成图块并创建低分辨率图像。首先在最低层显示低分辨率版本,然后在加载时将图块加载到顶部绘制它们?

【讨论】:

【参考方案7】:

您可以使用图像的 URL 和 startDownload 方法创建 UIImageView 的子类。 这是一个基本示例,必须改进。

@property (nonatomic, strong) NSURL *imageURL;
- (void)startDownload;

@implementation ImgeViewSubClass

    NSURLConnection *connection; 
    NSMutableData *imageData;


开始下载方法:

- (void)startDownload
 
    NSURLRequest *request = [NSURLRequest requestWithURL:imageURL];
    connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
    [connection start];
    imageData = [NSMutableData data];


来自NSURLConnectionDataDelegate的委托方法

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^
        @synchronized(imageData)
        
            [imageData appendData:data];
        

        // this part must be improved using CGImage instead of UIImage because we are not on main thread
        UIImage *image = [UIImage imageWithData:imageData];
        if (image) 
            [self performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO];
        
    );


【讨论】:

【参考方案8】:

答案在 ImageIO.framework 中,其实很简单

    首先创建一个 CGImageSourceRef mySource ,使用 CGImageSourceCreateIncremental() 实例化它。

    使用图像 URL 设置并启动 NSURLConnection。

    在 connection:didReceiveData: 中,将接收到的数据附加到占位符数据中,并通过调用更新图像源

CGImageSourceUpdateData(imageSource, (CFDataRef)imageData, NO);

然后将图像的部分加载部分加载到您的 UIImageView

self.image = [UIImage imageWithCGImage:CGImageSourceCreateImageAtIndex(imageSource, 0, nil)];

    在connectionDidFinishLoading中:通过调用完成

    CGImageSourceUpdateData(imageSource, (CFDataRef)imageData, YES);

    self.image = [UIImage imageWithCGImage:CGImageSourceCreateImageAtIndex(imageSource, 0, nil)];

    CFRelease(imageSource);

    imageData = nil;

这是我编写的示例代码:

https://github.com/mohammedDehairy/MDIncrementalImageView

【讨论】:

【参考方案9】:

为什么不使用 ASIHTTPRequest 请求:

#import "ASIHTTPRequest.h"

这将有助于在后台加载/绘制,也可以执行其他任务。

试试这个:

#import "ASIHTTPRequest.h"

[self performSelectorInBackground:@selector(DownLoadImageInBackground:)
   withObject:YOUR IMAGE ARRAY];

-(void) DownLoadImageInBackground:(NSArray *)imgUrlArr1

 NSURL * url = [Image URL];
 ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
 [request setDelegate:self];
 [request startAsynchronous];


-(void)requestFailed:(ASIHTTPRequest *)request

 NSLog(@"URL Fail : %@",request.url);
 NSError *error = [request error];
 // you can give here alert too..


-(void)requestFinished:(ASIHTTPRequest *)request


///////////  Drawing Code Here////////////////////
NSData *responseData = [request responseData];
UIImage *imgInBackground = [[UIImage alloc] initWithData:responseData];
[imageView setImage: imgInBackground];

【讨论】:

感谢您的回复,但我希望图像逐步显示。只有在我收到所有数据后,您的代码才会显示图像。 我没有太多经验...我认为您可以使用网络视图而不是图像视图。有它的委托方法。将在此工作以显示 UIImage 从服务器逐步显示...【参考方案10】:

我不确定您的代码的其他部分(注册此模块)是如何实现的,但请尝试以下操作,

尝试使用此选择器并将运行循环模式设置为NSDefaultRunLoopMode

[self performSelectorOnMainThread:@selector(processImage:)
                   withObject:objParameters
               waitUntillDone:NO
                        modes:[NSArray arrayWithObject:NSDefaultRunLoopMode]]

此执行将释放您的 UI 交互,如果有帮助请告诉我。

欲了解更多信息:APPLE DOCS

【讨论】:

正如您在上述线程中提到的,您需要异步下载大约 40-50 张图像,这应该会有所帮助。让我知道它,成功时可能是最佳实践。【参考方案11】:
//JImage.h

#import <Foundation/Foundation.h>


@interface JImage : UIImageView 

    NSURLConnection *connection;

    NSMutableData* data;

    UIActivityIndicatorView *ai;


-(void)initWithImageAtURL:(NSURL*)url;  

@property (nonatomic, retain) NSURLConnection *connection;

@property (nonatomic, retain) NSMutableData* data;

@property (nonatomic, retain) UIActivityIndicatorView *ai;

@end



//JImage.m

#import "JImage.h"

@implementation JImage
@synthesize ai,connection, data;

-(void)initWithImageAtURL:(NSURL*)url 


    [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;

    [self setContentMode:UIViewContentModeScaleToFill];

    if (!ai)

        [self setAi:[[UIActivityIndicatorView alloc]   initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]]; 

        [ai startAnimating];

        [ai setFrame:CGRectMake(27.5, 27.5, 20, 20)];

        [ai setColor:[UIColor blackColor]];

        [self addSubview:ai];
    
    NSURLRequest* request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60];

    connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];    



- (void)connection:(NSURLConnection *)theConnection didReceiveData:(NSData *)incrementalData 

   if (data==nil) 
       data = [[NSMutableData alloc] initWithCapacity:5000];

   [data appendData:incrementalData];

   NSNumber *resourceLength = [NSNumber numberWithUnsignedInteger:[data length]];

   NSLog(@"resourceData length: %d", [resourceLength intValue]);


- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error 

    NSLog(@"Connection error...");

    [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;

    [ai removeFromSuperview];


- (void)connectionDidFinishLoading:(NSURLConnection*)theConnection 

    [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;

    [self setImage:[UIImage imageWithData: data]];

    [ai removeFromSuperview];   

@end



//Include the definition in your class where you want to use the image
-(UIImageView*)downloadImage:(NSURL*)url:(CGRect)frame 

    JImage *photoImage=[[JImage alloc] init]; 

    photoImage.backgroundColor = [UIColor clearColor]; 

   [photoImage setFrame:frame];

   [photoImage setContentMode:UIViewContentModeScaleToFill]; 

   [photoImage initWithImageAtURL:url];

   return photoImage;




//call the function
UIImageView *imagV=[self downloadImage:url :rect]; 

//you can call the downloadImage function in looping statement and subview the returned  imageview. 
//it will help you in lazy loading of images.


//Hope this will help

【讨论】:

嗨,Kuldeep,感谢分享答案,但这不是我想要的方式。我想在 didReceiveData 方法中检索图像时逐步显示图像。所以我想在 didReceiveData 中设置图像,它会挂起应用程序直到设置图像。我想要相同的功能而不需要挂起交互。

以上是关于UIImage 从服务器逐步显示的主要内容,如果未能解决你的问题,请参考以下文章

如何在swift中从UIImage获取图像扩展/格式

UIImageView 不显示从通知中获取的 UIImage

UIImage 的 -imageNamed: 方法是不是适用于存储在“文档”中的图像文件?

UIImage 没有从相机中显示,只是从照片库中显示

通过 NSOutputStream 发送 UIImage [关闭]

UIImage 在下载时损坏