web-dev-qa-db-ja.com

今、drawRectを正しく機能させる方法はありますか?

元の質問............................................... ................

DrawRectの上級ユーザーであれば、当然、drawRectは「すべての処理が完了する」まで実際には実行されません。

setNeedsDisplayは、ビューに無効なフラグとOSのフラグを立て、基本的にすべての処理が完了するまで待機します。これは、次のような一般的な状況で腹立たしいことがあります。

  • ビューコントローラ1
  • いくつかの機能を開始します2
  • 増分3
    • ますます複雑なアートワークを作成し、4
    • 各ステップでsetNeedsDisplay(間違った!)5
  • すべての作業が完了するまで6

もちろん、上記の1〜6を実行すると、drawRectが実行される1回だけステップ6の後。

あなたの目標は、ビューがポイント5で更新されることです。


元の質問に対する解決策............................. .................

Wordでは、(A)背景大きな絵を描くことができ、前景を呼び出すことができますUIの更新または(B)議論の余地のある議論使用しない4つの「即時」メソッドが提案されていますバックグラウンドプロセス。動作する結果については、デモプログラムを実行してください。 5つのメソッドすべてに#definesがあります。


Tom Swiftによって導入された本当に驚くべき代替ソリューション..................

トムSwiftは、実行ループを操作するの驚くべきアイデアを説明しています。実行ループをトリガーする方法は次のとおりです。

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

これは本当に素晴らしいエンジニアリングです。もちろん、実行ループを操作するときは非常に注意する必要があり、このアプローチは専門家向けのものであると多くの人が指摘しています。


発生する奇妙な問題............................. .................

いくつかのメソッドは機能しますが、デモではっきりとわかる奇妙なプログレッシブスローダウンアーティファクトがあるため、実際には「機能」しません。

下に貼り付けた「回答」までスクロールして、コンソールの出力を表示します。速度が徐々に低下する様子を確認できます。

新しいSO質問です:
実行ループ/ drawRectでの不可解な「進行性の減速」問題

ここにデモアプリのV2があります...
http://www.fileswap.com/dl/p8lU3gAi/stepwiseDrawingV2.Zip.html

5つのメソッドすべてがテストされることがわかります。

#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はそれぞれ、驚異的なソリューションを他の特定の質問、それが役に立てば幸いです。

44
Fattie

ユーザーインターフェイスの更新は、実行ループの現在のパスの最後に行われます。これらの更新はメインスレッドで実行されるため、メインスレッドで長時間実行されるもの(長い計算など)は、インターフェイスの更新が開始されないようにします。さらに、メインスレッドでしばらく実行されると、タッチ処理が応答しなくなります。

これは、メインスレッドで実行されているプロセスの他のある時点からUIの更新を「強制」する方法がないことを意味します。 トムの答えが示すように、前のステートメントは完全に正しいわけではありません。メインスレッドで実行される操作の途中で実行ループが完了するようにすることができます。ただし、これでもアプリケーションの応答性が低下する可能性があります。

一般に、実行に時間がかかるものはすべてバックグラウンドスレッドに移動して、ユーザーインターフェイスが応答性を維持できるようにすることをお勧めします。ただし、UIに対して実行する更新はすべて、メインスレッドで実行する必要があります。

おそらく、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上記の一部は、長い計算、または複数の値をループする操作である可能性があります。この例では、UIは操作の最後にのみ更新されますが、UIで継続的な進行状況の追跡が必要な場合は、UI更新を実行する必要がある場合はいつでも、ディスパッチをメインキューに配置できます。

古いバージョンのOSでは、手動で、またはNSOperationを介してバックグラウンドスレッドを中断できます。手動のバックグラウンドスレッドの場合は、

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

または

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

そして、あなたが使用できるUIを更新するには

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

前のメソッドのNO引数は、継続的な進行状況バーを処理している間、UIを定期的に更新するために必要であることに注意してください。

このサンプルアプリケーション クラス用に作成したものは、NSOperationsとキューの両方を使用してバックグラウンド作業を実行し、完了時にUIを更新する方法を示しています。また、my Molecules アプリケーションは、新しい構造を処理するためにバックグラウンドスレッドを使用し、ステータスバーが進行すると更新されます。ソースコードをダウンロードして、私がこれをどのように達成したかを確認できます。

27
Brad Larson

私があなたの質問を正しく理解していれば、これに対する簡単な解決策があります。実行時間の長いルーチンの間に、現在の実行ループに、独自の処理の特定のポイントで1回(またはそれ以上)のループを処理するように指示する必要があります。たとえば、表示を更新したい場合。ダーティ更新領域のあるビューには、runloopを実行したときに呼び出されるdrawRect:メソッドがあります。

現在のrunloopに1回の反復処理を実行するように指示するには(そして次に戻ります...):

[[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];
}

そして、これはこのテクニックを示す完全に機能するサンプルです

多少の調整を行うと、おそらくこれを調整して、残りのUI(つまりユーザー入力)も応答性を高めるようにすることができます。

更新(この手法を使用する場合の注意)

このソリューション(runMode:を呼び出して、drawRect:を強制的に呼び出す)は必ずしも優れたアイデアではない、という他のユーザーからのフィードバックの多くに同意するだけです。私はこの質問に答えたが、私が述べた質問に対する実際の「ここにある」という答えは事実であり、これを「正しい」アーキテクチャとして宣伝するつもりはない。また、同じ効果を実現する他の(より良い)方法がないかもしれないと言っているわけではありません。確かに、私が知らなかった他のアプローチがあるかもしれません。

更新(ジョーのサンプルコードとパフォーマンスの質問への応答)

表示されるパフォーマンスの低下は、描画コードの各反復で実行ループを実行することによるオーバーヘッドです。これには、画面へのレイヤーのレンダリングや、入力の収集や処理など、実行ループの他のすべての処理が含まれます。

1つのオプションは、実行ループの起動頻度を下げることです。

別のオプションは、描画コードを最適化することです。現状では(これが実際のアプリなのか、それともサンプルなのかわかりません...)高速化するためにできることがいくつかあります。最初に行うことは、すべてのUIGraphicsGet/Save/Restoreコードをループの外に移動することです。

ただし、アーキテクチャの観点から、ここで説明した他のアプローチのいくつかを検討することを強くお勧めします。描画をバックグラウンドスレッド(アルゴリズムは変更されない)で発生するように構成できず、タイマーなどのメカニズムを使用してメインスレッドに信号を送り、描画が完了するまで一定の頻度でメインUIを更新できない理由はわかりません。ディスカッションに参加したほとんどの人は、これが「正しい」アプローチであることに同意するでしょう。

37
TomSwift

これはループで繰り返し行うことができ、正常に動作し、スレッドも実行ループもありません。

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

ループの前にすでに暗黙のトランザクションが存在している場合、これが機能する前に、[CATransaction commit]でそれをコミットする必要があります。

10

DrawRectが最も早く呼び出されるようにするために(OSが次のハードウェアディスプレイの更新などまで待機する可能性があるため、必ずしもすぐである必要はありません)、アプリはUI実行ループをできるだけ早くアイドル状態にする必要があります。 UIスレッドのすべてのメソッドを終了し、ゼロ以外の時間。

メインスレッドでこれを行うには、アニメーションフレーム時間以上かかる処理を短いチャンクに切り刻み、短い遅延の後でのみ継続的な作業をスケジュールします(そのため、drawRectがギャップで実行される可能性があります)。バックグラウンドスレッド。performSelectorOnMainThreadを定期的に呼び出して、適切なアニメーションフレームレートでsetNeedsDisplayを実行します。

OpenGL以外の方法でディスプレイをすぐに更新する(次のハードウェアディスプレイの更新が1〜3回行われる)には、目に見えるCALayerの内容を、描画した画像またはCGBitmapと交換します。アプリは、ほとんどいつでも、Core GraphicsビットマップへのQuartz描画を実行できます。

新しく追加された答え:

この解決策へのヒントとして、以下のブラッドラーソンのコメントとクリストファーロイドの別の回答に関するコメントをご覧ください。

[ CATransaction flush ];

uI実行ループをブロックしているメソッド内からフラッシュが実行された場合でも、setNeedsDisplayリクエストが実行されたビューでdrawRectが呼び出されます。

UIスレッドをブロックする場合、変更中のCALayerコンテンツも更新するためにCore Animationフラッシュが必要であることに注意してください。したがって、進行状況を示すためにグラフィックコンテンツをアニメーション化する場合、これらは両方とも同じものの形式になる可能性があります。

上記の新しく追加された回答への新しい追加メモ:

DrawRectまたはアニメーションの描画が完了するよりも速くフラッシュしないでください。フラッシュがキューに入れられ、奇妙なアニメーション効果が発生する可能性があります。

9
hotpaw2

これの知恵(あなたがすべきこと)を疑うことなく、次のことができます。

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

-setNeedsDisplayは、ビューを再描画する必要があるとしてマークします。 -displayIfNeededは、ビューのバッキングレイヤーを強制的に再描画しますが、表示する必要があるとマークされている場合のみです。

ただし、ここでの質問は、一部のリワークを使用できるアーキテクチャを示していることを強調しておきます。非常にまれなケースを除いて、すべてビューをすぐに再描画する必要がない、または強制したくない。そのユースケースを念頭に置いてビルドされていないUIKit。うまくいくなら、幸運だと考えてください。

4
Dave DeLong

セカンダリスレッドで重い処理を実行し、メインスレッドにコールバックしてビューの更新をスケジュールしましたか? 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];

}
1
kubi

私はこれが古いスレッドであることを理解していますが、与えられた問題に対する明確な解決策を提供したいと思います。

理想的な状況では、すべての重い作業はバックグラウンドスレッドで行う必要があるという他のポスターにも同意します。 UIKitが提供するもの。私の場合、UIの初期化には時間がかかり、バックグラウンドで実行できるものは何もないため、初期化中にプログレスバーを更新するのが最善の方法です。

ただし、理想的なGCDアプローチについて考えると、解決策は実際には簡単です。すべての作業をバックグラウンドスレッドで実行し、メインスレッドで同期的に呼び出されるチャックに分割します。実行ループはチャックごとに実行され、UIやプログレスバーなどを更新します。

- (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 = ...: 
        });

        ...
    });
}

もちろん、これは基本的にBradの手法と同じですが、彼の答えは当面の問題に完全には対応していません。UIを定期的に更新しながら、スレッドセーフでないコードを多数実行するという問題です。

1
tarmes

私が思うに、最も完全な答えはジェフリー・サンベルのブログ投稿 'Grand Central Dispatchを使用したiOSの非同期操作' であり、それは私にとってうまくいきました!これは基本的に、上記のBradによって提案されたものと同じソリューションですが、OSX/IOS同時実行モデルに関して完全に説明されています。

dispatch_get_current_queue関数は、ブロックがディスパッチされる現在のキューを返し、dispatch_get_main_queue関数は、UIが実行されているメインキューを返します。

dispatch_get_main_queue関数は、iOSアプリのUIを更新するのに非常に役立ちます。これは、UIKitメソッドがスレッドセーフではないためです(いくつかの例外があります)。UI要素を更新するための呼び出しは、常にメインから実行する必要がありますキュー。

典型的な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);
            });
        }
    }
});

このサンプルの最初の部分は、Damianの回答で here にあります。

1
Jakub

Joe-長い処理がすべて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
0