44

drawRect の上級ユーザーであれば、もちろん、「すべての処理が終了する」まで実際には drawRect が実行されないことを知っているでしょう。

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

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

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

あなたの目標は、ポイント 5 でビューを更新することです。何をしますか?

4

10 に答える 10

37

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

現在の実行ループに 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: の呼び出しを強制する) が必ずしも優れたアイデアではないという、ここにいる他のユーザーからのフィードバックの多くに同意すると言いたいだけです。私はこの質問に、述べられた質問に対する事実に基づく「方法は次のとおり」であると感じて答えましたが、これを「正しい」アーキテクチャとして宣伝するつもりはありません。また、同じ効果を達成するための他の (より良い?) 方法がないかもしれないと言っているわけではありません。確かに、私が知らなかった他のアプローチがあるかもしれません。

更新 (Joe のサンプル コードとパフォーマンスに関する質問への回答)

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

1 つのオプションとして、実行ループの呼び出し頻度を下げることが考えられます。

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

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

于 2011-01-22T03:40:46.813 に答える
27

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

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

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

おそらく、SnowLeopardとiOS4.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を更新する方法を示しています。また、私のMoleculesアプリケーションは、新しい構造を処理するためにバックグラウンドスレッドを使用し、ステータスバーは進行に応じて更新されます。ソースコードをダウンロードして、私がこれをどのように達成したかを確認できます。

于 2011-01-19T19:53:02.197 に答える
11

これはループ内で繰り返し実行でき、スレッドがなく、runloopをいじることなく、正常に機能します。

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

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

于 2011-01-23T06:10:02.977 に答える
9

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

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

OpenGL を使用せずにディスプレイをほぼ即座に更新する方法 (つまり、次のハードウェア ディスプレイの更新時または 3 回目) は、表示されている CALayer コンテンツを、描画した画像または CGBitmap と交換することです。アプリは、ほぼいつでも Core Graphics ビットマップへの Quartz 描画を行うことができます。

新しく追加された回答:

以下の Brad Larson のコメントと、別の回答に関する Christopher Lloyd のコメントを、この解決策につながるヒントとしてご覧ください。

[ CATransaction flush ];

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

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

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

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

于 2011-01-19T20:25:08.773 に答える
4

これの賢明さを疑うことなく(あなたがすべきことです)、次のことができます:

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

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

ただし、あなたの質問は、再加工を使用できるアーキテクチャを示していることを強調します。非常にまれなケースを除いて、ビューをすぐに再描画する必要はなく、強制的に再描画する必要もありません。そのユースケースを念頭に置いて構築されていないUIKitで、それが機能する場合は、幸運だと考えてください.

于 2011-01-23T19:08:02.147 に答える
1

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

}
于 2011-01-19T19:47:36.400 に答える
1

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

理想的な状況では、すべての重労働をバックグラウンドスレッドで実行する必要があるという他のポスターに同意しますが、時間のかかる部分では、次のような非スレッドセーフメソッドへのアクセスが多数必要になるため、これが単に不可能な場合があります。 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 を定期的に更新しながら多くの非スレッド セーフ コードを実行するという問題に完全には対応していません。

于 2012-10-02T16:02:22.327 に答える
0

ジョー -- 時間のかかる処理がすべて drawRect 内で行われるように設定したい場合は、それを機能させることができます。私はちょうどテストプロジェクトを書きました。できます。以下のコードを参照してください。

LongyComputationTestAppDelegate.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;

インクリメンタ.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
于 2011-01-19T20:56:35.370 に答える
0

元の問題について:

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


トム・スウィフトごとに交互に

Tom Swift は、非常に単純に実行ループを操作するという驚くべきアイデアを説明しました。実行ループをトリガーする方法は次のとおりです。

[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate 日付]];

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


しかし、奇妙な問題が発生します...

多くの方法は機能しますが、実際には「機能」しません。これは、デモではっきりと確認できる、奇妙なプログレッシブ スローダウン アーティファクトがあるためです。

下に貼り付けた「回答」までスクロールして、コンソール出力を表示します。徐々に遅くなる様子がわかります。

これが新しい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はそれぞれ、他の特定の質問で驚くべき解決策を提示しています。

于 2021-11-29T14:53:13.057 に答える