Есть ли способ заставить drawRect работать прямо сейчас? - PullRequest
43 голосов
/ 19 января 2011

Оригинальный вопрос ...............................................

Если вы являетесь опытным пользователем drawRect, вы будете знать, что drawRect, конечно же, не будет работать, пока "вся обработка не будет завершена".

setNeedsDisplay помечает представление как недействительное и ОС и в основном ждет, пока не будет выполнена вся обработка.Это может приводить в бешенство в обычной ситуации, когда вы хотите иметь:

  • контроллер представления 1
  • запускает некоторую функцию 2
  • , которая постепенно увеличивается на 3
    • создает все более и более сложные графические объекты и 4
    • на каждом шаге, вы устанавливаетеNeedsDisplay (неправильно!) 5
  • до тех пор, пока вся работа не будет выполнена 6

Конечно, когда вы делаете выше 1-6, все, что происходит, это то, что drawRect запускается только один раз после шага 6.

Ваша цель - обновить представление в точке 5. Что делать?


Решение исходного вопроса ..............................................

Одним словом, выможет (A) фон большая картина, и вызвать на первый план для обновления пользовательского интерфейса или (в), возможно, спорное есть четыре«немедленные» методы предложили не использовать фоновый процесс.Для результата того, что работает, запустите демонстрационную программу.У него есть #defines для всех пяти методов.


Действительно поразительное альтернативное решение, представленное Томом Свифтом ..................

Том Свифт объяснил удивительную идею простого манипулирования циклом выполнения .Вот как вы запускаете цикл выполнения:

[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [дата NSDate]];

Это действительно удивительный пример разработки.Конечно, нужно быть очень осторожным при манипулировании циклом выполнения, и, как многие отмечали, этот подход предназначен исключительно для экспертов.


Странная проблема, которая возникает ..............................................

Несмотря на то, что некоторые методы работают, они на самом деле не «работают», потому что есть странный прогрессирующий замедляющий артефакт, который вы четко увидите в демоверсии.

Прокрутите до «ответа»Я вставил ниже, показывая вывод консоли - вы можете видеть, как он постепенно замедляется.

Вот новый вопрос SO:
Таинственная проблема "прогрессивного замедления" в цикле выполнения / drawRect

Вот V2 демонстрационного приложения ...
http://www.fileswap.com/dl/p8lU3gAi/stepwiseDrawingV2.zip.html

Вы увидите, что он тестирует все пять методов,

#ifdef TOMSWIFTMETHOD
 [self setNeedsDisplay];
 [[NSRunLoop currentRunLoop]
      runMode:NSDefaultRunLoopMode beforeDate:[NSDate date]];
#endif
#ifdef HOTPAW
 [self setNeedsDisplay];
 [CATransaction flush];
#endif
#ifdef LLOYDMETHOD
 [CATransaction begin];
 [self setNeedsDisplay];
 [CATransaction commit];
#endif
#ifdef DDLONG
 [self setNeedsDisplay];
 [[self layer] displayIfNeeded];
#endif
#ifdef BACKGROUNDMETHOD
 // here, the painting is being done in the bg, we have been
 // called here in the foreground to inval
 [self setNeedsDisplay];
#endif
  • Вы можете сами увидеть, какие методы работают, а какие нет.

  • Вы можете увидеть причудливое «прогрессивное замедление».почему это происходит?

Так что подавляющая вещь - это странное «прогрессивное замедление»: на каждой итерации по неизвестным причинамВремя, затраченное на петлю смерти.Обратите внимание, что это относится как к тому, чтобы делать это «правильно» (фоновый вид), так и к использованию одного из «непосредственных» методов.


Практические решения ........................

Для тех, кто читает в будущем, если вы действительно не можете заставить его работать в производственном коде из-за «загадочного прогрессирующего замедления» ... Felz и Void представили поразительные решения в другом конкретном вопросе, надеюсь, этопомогает.

Ответы [ 9 ]

37 голосов
/ 22 января 2011

Если я правильно понимаю ваш вопрос, есть простое решение для этого.Во время вашей длительной подпрограммы вы должны указать текущему циклу выполнения обрабатывать одну итерацию (или более) цикла выполнения в определенных точках вашей собственной обработки.например, когда вы хотите обновить дисплей.Любые представления с грязными областями обновления будут иметь свои методы drawRect:, вызываемые при запуске runloop.

Чтобы указать текущему циклу выполнения обработать одну итерацию (а затем вернуться к вам ...):

[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];

Вот пример (неэффективной) длительной подпрограммы с соответствующим drawRect - каждый в контексте пользовательского UIView:

- (void) longRunningRoutine:(id)sender
{
    srand( time( NULL ) );

    CGFloat x = 0;
    CGFloat y = 0;

    [_path moveToPoint: CGPointMake(0, 0)];

    for ( int j = 0 ; j < 1000 ; j++ )
    {
        x = 0;
        y = (CGFloat)(rand() % (int)self.bounds.size.height);

        [_path addLineToPoint: CGPointMake( x, y)];

        y = 0;
        x = (CGFloat)(rand() % (int)self.bounds.size.width);

        [_path addLineToPoint: CGPointMake( x, y)];

        x = self.bounds.size.width;
        y = (CGFloat)(rand() % (int)self.bounds.size.height);

        [_path addLineToPoint: CGPointMake( x, y)];

        y = self.bounds.size.height;
        x = (CGFloat)(rand() % (int)self.bounds.size.width);

        [_path addLineToPoint: CGPointMake( x, y)];

        [self setNeedsDisplay];
        [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];
    }

    [_path removeAllPoints];
}

- (void) drawRect:(CGRect)rect
{
    CGContextRef ctx = UIGraphicsGetCurrentContext();

    CGContextSetFillColorWithColor( ctx, [UIColor blueColor].CGColor );

    CGContextFillRect( ctx,  rect);

    CGContextSetStrokeColorWithColor( ctx, [UIColor whiteColor].CGColor );

    [_path stroke];
}

А вот полностью рабочий пример, демонстрирующийэта техника .

С некоторыми изменениями вы, вероятно, сможете настроить ее так, чтобы остальная часть пользовательского интерфейса (т.е. пользовательский ввод) также реагировала.

Обновление (предостережение дляиспользуя эту технику)

Я просто хочу сказать, что я согласен с большей частью откликов других здесь, говорящих об этом решении (вызов runMode: принудительно вызвать вызов drawRect :) не обязательно является хорошимидея.Я ответил на этот вопрос тем, что я считаю фактическим ответом «вот как» на поставленный вопрос, и я не собираюсь рекламировать его как «правильную» архитектуру.Кроме того, я не говорю, что не может быть других (лучших?) Способов достижения того же эффекта - конечно, могут быть и другие подходы, о которых я не знал.

Обновление (ответ на пример кода Джо и вопрос о производительности)

Замедление производительности, которое вы видите, - это издержки запуска runloop на каждой итерации вашего чертежакод, который включает рендеринг слоя на экран, а также всю другую обработку, выполняемую runloop, такую ​​как сбор и обработка ввода.

Один из вариантов может состоять в том, чтобы вызывать runloop реже.

Другой вариант может заключаться в оптимизации вашего кода рисования.В его нынешнем виде (и я не знаю, является ли это вашим настоящим приложением или просто вашим образцом ...), есть несколько вещей, которые вы могли бы сделать, чтобы сделать его быстрее.Первое, что я хотел бы сделать, это переместить весь код UIGraphicsGet / Save / Restore за пределы цикла.

Однако с архитектурной точки зрения я бы настоятельно рекомендовал рассмотреть некоторые из других подходов, упомянутых здесь.Я не вижу причин, по которым вы не можете структурировать свой рисунок, чтобы он происходил в фоновом потоке (алгоритм не изменился), и используете таймер или другой механизм, чтобы сигнализировать основному потоку об обновлении его пользовательского интерфейса на некоторой частоте до тех пор, пока рисунок не будет завершен.Я думаю, что большинство людей, которые участвовали в обсуждении, согласились бы, что это был бы "правильный" подход.

27 голосов
/ 19 января 2011

Обновления пользовательского интерфейса происходят в конце текущего прохода через цикл выполнения.Эти обновления выполняются в основном потоке, поэтому все, что выполняется в основном потоке в течение длительного времени (длительные вычисления и т. Д.), Будет препятствовать запуску обновлений интерфейса.Кроме того, все, что выполняется некоторое время в главном потоке, также приведет к тому, что ваша обработка прикосновений не будет реагировать.

Это означает, что нет никакого способа «заставить» обновление пользовательского интерфейса выполнить какое-либо другое действие.точка в процессе, работающем в главном потоке. Предыдущий оператор не совсем корректен, как показывает ответ Тома.Вы можете позволить циклу выполнения завершиться в середине операций, выполняемых в основном потоке.Однако это все же может снизить скорость отклика вашего приложения.

В общем случае рекомендуется переместить все, что требует времени для выполнения, в фоновый поток, чтобы пользовательский интерфейс оставался отзывчивым.Однако любые обновления, которые вы хотите выполнить в пользовательском интерфейсе, необходимо выполнить обратно в главном потоке.

Возможно, самый простой способ сделать это в Snow Leopard и iOS 4.0+ - это использовать блоки, как показано нижеэлементарный пример:

dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
    // Do some work
    dispatch_async(main_queue, ^{
        // Update the UI
    });
});

Часть Do some work выше может быть длинным вычислением или операцией, которая зацикливается на нескольких значениях.В этом примере пользовательский интерфейс обновляется только в конце операции, но если вы хотите непрерывного отслеживания прогресса в своем пользовательском интерфейсе, вы можете поместить диспетчеризацию в основную очередь, где бы вам ни потребовалось выполнить обновление пользовательского интерфейса.

Для более старых версий ОС вы можете разорвать фоновый поток вручную или с помощью NSOperation.Для ручной работы в фоновом режиме вы можете использовать

[NSThread detachNewThreadSelector:@selector(doWork) toTarget:self withObject:nil];

или

[self performSelectorInBackground:@selector(doWork) withObject:nil];

, а затем для обновления пользовательского интерфейса вы можете использовать

[self performSelectorOnMainThread:@selector(updateProgress) withObject:nil waitUntilDone:NO];

Обратите внимание, что яобнаружил, что аргумент NO в предыдущем методе необходим для получения постоянных обновлений пользовательского интерфейса при работе с непрерывным индикатором выполнения.

Этот пример приложения , созданный для моего класса, иллюстрирует, как использовать обе NSOperationsи очереди для выполнения фоновой работы, а затем обновления пользовательского интерфейса, когда закончите.Кроме того, мое приложение Molecules использует фоновые потоки для обработки новых структур со строкой состояния, которая обновляется по мере этого.Вы можете скачать исходный код, чтобы увидеть, как я этого добился.

10 голосов
/ 23 января 2011

Вы можете делать это многократно в цикле, и он будет работать нормально, без потоков, без возни с циклом выполнения и т. Д.цикл, вам нужно зафиксировать это с помощью [CATransaction commit], прежде чем это сработает.

8 голосов
/ 19 января 2011

Для того, чтобы drawRect вызывался как можно быстрее (что не обязательно сразу, поскольку ОС все еще может ждать, например, до следующего обновления аппаратного дисплея и т. Д.), Приложение должно простаивать свой цикл выполнения пользовательского интерфейса, как тольковозможно, путем выхода из любого и всех методов в потоке пользовательского интерфейса, и на ненулевое количество времени.

Вы можете сделать это в основном потоке, выбрав любую обработку, которая занимает больше времени кадра анимациив более короткие порции и планирование продолжения работы только после небольшой задержки (поэтому drawRect может выполняться в промежутках) или путем обработки в фоновом потоке с периодическим вызовом executeSelectorOnMainThread для выполнения setNeedsDisplay с некоторой разумной частотой кадров анимации.

Не-OpenGL метод для немедленного обновления дисплея (что означает при ближайшем обновлении аппаратного дисплея или трех) - это замена видимого содержимого CALayer на изображение или CGBitmap, в который вы нарисовали.Приложение может рисовать Кварц в битовой карте Core Graphics практически в любое время.

Новый добавленный ответ:

Пожалуйста, смотрите комментарии Брэда Ларсона ниже и комментарий Кристофера Ллойда к другому ответу здесь какподсказка, ведущая к этому решению.

[ CATransaction flush ];

вызовет drawRect для представлений, для которых был выполнен запрос setNeedsDisplay, даже если сброс выполняется изнутри метода, который блокирует цикл выполнения пользовательского интерфейса.

Обратите внимание, что при блокировке потока пользовательского интерфейса необходима очистка Core Animation для обновления изменяющегося содержимого CALayer.Таким образом, для анимации графического содержимого, чтобы показать прогресс, оба могут оказаться формами одного и того же.

Новое добавленное примечание к новому добавленному ответу выше:

Не сбрасывайте быстрее, чем ваш drawRectили рисование анимации может быть завершено, так как это может привести к появлению очередей, вызывая странные эффекты анимации.

4 голосов
/ 23 января 2011

Не подвергая сомнению мудрость этого (что вам следует сделать), вы можете сделать:

[myView setNeedsDisplay];
[[myView layer] displayIfNeeded];

-setNeedsDisplay пометит представление как нуждающееся в перерисовке.-displayIfNeeded заставит фоновый слой представления перерисовываться, но только если он был помечен как необходимый для отображения.

Я подчеркну, однако, что ваш вопрос указывает на архитектуру, которая может использовать некоторые-за работой.Во всех, кроме исключительно редких случаях, никогда не требуется или не требуется принудительно перерисовывать представление .UIKit не создан с учетом этого варианта использования, и если он работает, считайте себя счастливчиком.

1 голос
/ 29 сентября 2014

Я думаю, самый полный ответ приходит из поста в блоге Джеффри Сэмбелла 'Асинхронные операции в iOS с Grand Central Dispatch' , и это сработало для меня!Это в основном то же решение, которое было предложено Брэдом выше, но полностью объясненное в терминах модели параллелизма OSX / IOS.

Функция dispatch_get_current_queue вернет текущую очередь, из которой отправляется блок, и *Функция 1007 * вернет основную очередь, в которой работает ваш пользовательский интерфейс.

Функция dispatch_get_main_queue очень полезна для обновления пользовательского интерфейса приложения iOS, поскольку методы UIKit не являются поточно-ориентированными (за некоторыми исключениями), поэтомулюбые вызовы, которые вы делаете для обновления элементов пользовательского интерфейса, всегда должны выполняться из главной очереди.

Типичный вызов GCD будет выглядеть примерно так:

// Doing something on the main thread
dispatch_queue_t myQueue = dispatch_queue_create("My Queue",NULL);
dispatch_async(myQueue, ^{

// Perform long running process   
dispatch_async(dispatch_get_main_queue(), ^{
    // Update the UI   
    }); 
}); 

// Continue doing other stuff on the  
// main thread while process is running.

И вот моя работапример (iOS 6+).Он отображает кадры сохраненного видео, используя класс AVAssetReader:

//...prepare the AVAssetReader* asset_reader earlier and start reading frames now:
[asset_reader startReading];

dispatch_queue_t readerQueue = dispatch_queue_create("Reader Queue", NULL);
dispatch_async(readerQueue, ^{
    CMSampleBufferRef buffer;
    while ( [asset_reader status]==AVAssetReaderStatusReading )
    {
        buffer = [asset_reader_output copyNextSampleBuffer];
        if (buffer!=nil)
        {
            //The point is here: to use the main queue for actual UI operations
            dispatch_async(dispatch_get_main_queue(), ^{
                // Update the UI using the AVCaptureVideoDataOutputSampleBufferDelegate style function
                [self captureOutput:nil didOutputSampleBuffer:buffer fromConnection:nil];
                CFRelease (buffer);
            });
        }
    }
});

Первая часть этого примера может быть найдена здесь в ответе Дамиана.

1 голос
/ 02 октября 2012

Я понимаю, что это старая ветка, но я бы хотел предложить чистое решение данной проблемы.

Я согласен с другими авторами, что в идеальной ситуации все тяжелые работы должны выполняться в фоновом режиме, однако бывают случаи, когда это просто невозможно, потому что трудоемкая часть требует большого доступа к не-поточно-безопасному методы, такие как предлагаемые UIKit. В моем случае инициализация моего пользовательского интерфейса занимает много времени, и я ничего не могу запустить в фоновом режиме, поэтому мой лучший вариант - обновить индикатор выполнения во время инициализации.

Однако, как только мы подумаем с точки зрения идеального подхода GCD, решение на самом деле будет простым. Мы выполняем всю работу в фоновом потоке, разделяя его на чаки, которые вызываются синхронно в основном потоке. Цикл выполнения будет выполняться для каждого патрона, обновляя интерфейс и все индикаторы выполнения и т. Д.

- (void)myInit
{
    // Start the work in a background thread.
    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        // Back to the main thread for a chunk of code
        dispatch_sync(dispatch_get_main_queue(), ^{
            ...

            // Update progress bar
            self.progressIndicator.progress = ...: 
        });

        // Next chunk
        dispatch_sync(dispatch_get_main_queue(), ^{
            ...

            // Update progress bar
            self.progressIndicator.progress = ...: 
        });

        ...
    });
}

Конечно, это, в сущности, то же самое, что и метод Брэда, но его ответ не совсем решает существующую проблему - запуск большого количества не поточно-безопасного кода при периодическом обновлении пользовательского интерфейса.

1 голос
/ 19 января 2011

Вы пытались выполнить тяжелую обработку во вторичном потоке и перезванивать в основной поток, чтобы запланировать обновления просмотра?NSOperationQueue делает подобные вещи довольно простыми.


Пример кода, который принимает массив NSURL в качестве входных данных и асинхронно загружает их все, уведомляя основной поток о завершении и сохранении каждого из них.1005 *

- (void)fetchImageWithURLs:(NSArray *)urlArray {
    [self.retriveAvatarQueue cancelAllOperations];
    self.retriveAvatarQueue = nil;

    NSOperationQueue *opQueue = [[NSOperationQueue alloc] init];

    for (NSUInteger i=0; i<[urlArray count]; i++) {
        NSURL *url = [urlArray objectAtIndex:i];

        NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:@selector(cacheImageWithIndex:andURL:)]];
        [inv setTarget:self];
        [inv setSelector:@selector(cacheImageWithIndex:andURL:)];
        [inv setArgument:&i atIndex:2];
        [inv setArgument:&url atIndex:3];

        NSInvocationOperation *invOp = [[NSInvocationOperation alloc] initWithInvocation:inv];
        [opQueue addOperation:invOp];
        [invOp release];
    }

    self.retriveAvatarQueue = opQueue;
    [opQueue release];
}

- (void)cacheImageWithIndex:(NSUInteger)index andURL:(NSURL *)url {
    NSData *imageData = [NSData dataWithContentsOfURL:url];

    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSString *filePath = PATH_FOR_IMG_AT_INDEX(index);
    NSError *error = nil;

    // Save the file      
    if (![fileManager createFileAtPath:filePath contents:imageData attributes:nil]) {
        DLog(@"Error saving file at %@", filePath);
    }

    // Notifiy the main thread that our file is saved.
    [self performSelectorOnMainThread:@selector(imageLoadedAtPath:) withObject:filePath waitUntilDone:NO];

}
0 голосов
/ 19 января 2011

Джо - если вы хотите настроить его так, чтобы ваша длительная обработка происходила внутри drawRect, вы можете заставить его работать.Я только что написал тестовый проект.Оно работает.Смотрите код ниже.

LengthyComputationTestAppDelegate.h:

#import <UIKit/UIKit.h>
@interface LengthyComputationTestAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;

@end

LengthComputationTestAppDelegate.m:

#import "LengthyComputationTestAppDelegate.h"
#import "Incrementer.h"
#import "IncrementerProgressView.h"

@implementation LengthyComputationTestAppDelegate

@synthesize window;


#pragma mark -
#pragma mark Application lifecycle

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    

    // Override point for customization after application launch.
    IncrementerProgressView *ipv = [[IncrementerProgressView alloc]initWithFrame:self.window.bounds];
    [self.window addSubview:ipv];
    [ipv release];
    [self.window makeKeyAndVisible];
    return YES;
}

Incrementer.h:

#import <Foundation/Foundation.h>

//singleton object
@interface Incrementer : NSObject {
    NSUInteger theInteger_;
}

@property (nonatomic) NSUInteger theInteger;

+(Incrementer *) sharedIncrementer;
-(NSUInteger) incrementForTimeInterval: (NSTimeInterval) timeInterval;
-(BOOL) finishedIncrementing;

incrementer.m:

#import "Incrementer.h"

@implementation Incrementer

@synthesize theInteger = theInteger_;

static Incrementer *inc = nil;

-(void) increment {
    theInteger_++;
}

-(BOOL) finishedIncrementing {
    return (theInteger_>=100000000);
}

-(NSUInteger) incrementForTimeInterval: (NSTimeInterval) timeInterval {
    NSTimeInterval negativeTimeInterval = -1*timeInterval;
    NSDate *startDate = [NSDate date];
    while (!([self finishedIncrementing]) && [startDate timeIntervalSinceNow] > negativeTimeInterval)
        [self increment];
    return self.theInteger;
}

-(id) init {
    if (self = [super init]) {
        self.theInteger = 0;
    }
    return self;
}

#pragma mark --
#pragma mark singleton object methods

+ (Incrementer *) sharedIncrementer { 
    @synchronized(self) {
        if (inc == nil) {
            inc = [[Incrementer alloc]init];        
        }
    }
    return inc;
}

+ (id)allocWithZone:(NSZone *)zone {
    @synchronized(self) {
        if (inc == nil) {
            inc = [super allocWithZone:zone];
            return inc;  // assignment and return on first allocation
        }
    }
    return nil; // on subsequent allocation attempts return nil
}

- (id)copyWithZone:(NSZone *)zone
{
    return self;
}

- (id)retain {
    return self;
}

- (unsigned)retainCount {
    return UINT_MAX;  // denotes an object that cannot be released
}

- (void)release {
    //do nothing
}

- (id)autorelease {
    return self;
}

@end

IncrementerProgressView.m:

#import "IncrementerProgressView.h"


@implementation IncrementerProgressView
@synthesize progressLabel = progressLabel_;
@synthesize nextUpdateTimer = nextUpdateTimer_;

-(id) initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame: frame]) {
        progressLabel_ = [[UILabel alloc]initWithFrame:CGRectMake(20, 40, 300, 30)];
        progressLabel_.font = [UIFont systemFontOfSize:26];
        progressLabel_.adjustsFontSizeToFitWidth = YES;
        progressLabel_.textColor = [UIColor blackColor];
        [self addSubview:progressLabel_];
    }
    return self;
}

-(void) drawRect:(CGRect)rect {
    [self.nextUpdateTimer invalidate];
    Incrementer *shared = [Incrementer sharedIncrementer];
    NSUInteger progress = [shared incrementForTimeInterval: 0.1];
    self.progressLabel.text = [NSString stringWithFormat:@"Increments performed: %d", progress];
    if (![shared finishedIncrementing])
        self.nextUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:0. target:self selector:(@selector(setNeedsDisplay)) userInfo:nil repeats:NO];
}

- (void)dealloc {
    [super dealloc];
}

@end
...