Подтвердить что ты не робот

Есть ли способ сделать работу drawRect прямо сейчас?

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

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

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

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

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

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


Решение первоначального вопроса..............................................

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


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

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

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

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


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

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

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

Здесь новый вопрос SO:
Таинственное "постепенное замедление" проблема в run loop/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
  • Вы можете сами убедиться, какие методы работают, а какие нет.

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

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

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


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

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

4b9b3361

Ответ 1

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

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

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

Возможно, самый простой способ сделать это в 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 использует фоновые потоки для обработки новых структур, при этом строка состояния обновляется по мере продвижения. Вы можете загрузить исходный код, чтобы узнать, как я это сделал.

Ответ 2

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

Чтобы сообщить текущему 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 за пределы цикла.

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

Ответ 3

Вы можете сделать это несколько раз в цикле, и он будет работать нормально, без потоков, без взаимодействия с runloop и т.д.

[CATransaction begin];
// modify view or views
[view setNeedsDisplay];
[CATransaction commit];

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

Ответ 4

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

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

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

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

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

[ CATransaction flush ];

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

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

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

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

Ответ 5

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

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

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

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

Ответ 6

Пробовали ли вы выполнять тяжелую обработку вторичного потока и переадресовывать основной поток для планирования обновлений? NSOperationQueue делает это довольно легко.


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

- (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];

}

Ответ 7

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

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

Однако, как только мы будем думать о идеальном подходе 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 = ...: 
        });

        ...
    });
}

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

Ответ 8

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

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

Функция 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);
            });
        }
    }
});

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

Ответ 9

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

LengthyComputTestAppDelegate.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