iOS 中的竞态条件对 UICollectionView 使用 GCD

Race conditions in iOS using GCD for UICollectionView

我希望有人能帮我解决这段代码,并尝试找出优化它的最佳方法,这样我就不会遇到太多竞争条件。

我知道正在发生的一些事情:

我只是认为总体上可以使这段代码更有效率。如果有人有任何建议,我将不胜感激。

我将post这里的大部分代码,我已经删除了很多处理以节省行数,因此阅读起来并不难,但我想我留下的足够多了,所以你可以查看在哪里使用了 GCD 以及我是否做错了。如果您想查看全部内容,我还将 link 将完整代码放在要点中。

Gist

#import "DashboardCollectionViewController.h"
#import "DashboardLayout.h"
#import "HeaderViewCell.h"
#import "DashboardCell.h"
#import "DashboardDetailViewController.h"
#import "PreferencesManager.h"
#import "UIColor+HexColors.h"
#import "PNChart.h"

@interface DashboardCollectionViewController () <UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UICollectionViewDelegate>

@property (nonatomic, strong) DashboardLayout *listLayout;
@property (nonatomic, strong) DashboardLayout *gridLayout;
@property (strong, nonatomic) HKHealthStore *healthStore;
@property (strong, nonatomic) NSDictionary *dataTypes;
@property (strong, nonatomic) HeaderViewCell *headerCell;

@end

@implementation DashboardCollectionViewController
{

    NSString *layoutType;
    NSString *unitPreference;
    NSMutableDictionary *itemData;
    UIRefreshControl *refreshControl;
    NSArray *sortedDashboardItems;
    __block NSMutableArray *graphData;

}

@synthesize dashboardItems, allDashboardItems, myCollectionView, dataTypes, headerCell;

- (void)viewDidLoad {

    [super viewDidLoad];
    // Do any additional setup after loading the view.

    [self setupFlowLayoutA];
    [self setupFlowLayoutB];

    layoutType = @"listView";
    dataTypes = [NSDictionary dictionaryWithObjectsAndKeys:
                 @1, HKQuantityTypeIdentifierStepCount,
                 @2, HKQuantityTypeIdentifierFlightsClimbed,
                 @3, HKQuantityTypeIdentifierDistanceWalkingRunning,
                 @4, HKQuantityTypeIdentifierActiveEnergyBurned,
                 @5, HKQuantityTypeIdentifierBodyMass,
                 @6, HKQuantityTypeIdentifierDistanceCycling,
                 @7, HKQuantityTypeIdentifierHeartRate,
                 @8, HKQuantityTypeIdentifierBodyMassIndex, nil];

    UINib *headerNib = [UINib nibWithNibName:@"HeaderView" bundle:nil];
    UINib *cellNib = [UINib nibWithNibName:@"DashboardCell" bundle:nil];
    DashboardLayout *dashboardLayout = [self listLayout];

    [myCollectionView registerNib:headerNib forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"HeaderCell"];
    [myCollectionView registerNib:cellNib forCellWithReuseIdentifier:@"Cell"];
    [myCollectionView setCollectionViewLayout:dashboardLayout animated:YES];

    //check if health kit is available on this device
    if ([HKHealthStore isHealthDataAvailable]) {

        if (!self.healthStore) {
            self.healthStore = [HKHealthStore new];
        }
    }

    refreshControl = [UIRefreshControl new];
    [refreshControl addTarget:self action:@selector(refreshAllData) forControlEvents:UIControlEventValueChanged];
    [myCollectionView addSubview:refreshControl];

}

- (void)viewWillAppear:(BOOL)animated {

    [super viewWillAppear:animated];

    //set data types we want to read and write
    NSSet *dataTypesToWrite = [self dataTypesToWrite];
    NSSet *datatTypesToRead = [self dataTypesToRead];

    allDashboardItems = [self loadDashboardItems];
    unitPreference = [self loadUnitPreferences];
    dashboardItems = [NSMutableArray new];
    itemData = [NSMutableDictionary new];

    for (NSDictionary *item in allDashboardItems) {

        NSString *enabled = item[@"enabled"];

        if ([enabled isEqualToString:@"1"]) {
            [dashboardItems addObject:item];
        }

    }

    NSSortDescriptor *descriptor = [[NSSortDescriptor alloc] initWithKey:@"order" ascending:YES];
    sortedDashboardItems = [dashboardItems sortedArrayUsingDescriptors:@[descriptor]];

    graphData = [[NSMutableArray alloc] initWithCapacity:[sortedDashboardItems count]];

    for (int i=0; i < [sortedDashboardItems count]; i++) {

        [graphData addObject:[NSNull null]];

    }

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
#if DEBUG
    NSLog(@"%@", [defaults objectForKey:@"unitPreference"]);
#endif

    //request authorization
    [self.healthStore requestAuthorizationToShareTypes:dataTypesToWrite readTypes:datatTypesToRead completion:^(BOOL success, NSError *error) {
        if (!success) {
            //user did not authorize healthkit
            NSLog(@"Health Kit was not given the correct permissions");
            return;
        } else {

            [self refreshData];
            [self refreshGraphs];
           // [myCollectionView reloadData];

        }
    }];

    [self refreshData];
    [self refreshGraphs];
//    [myCollectionView reloadData];

}

- (void)refreshAllData {

    [self refreshData];
    [self refreshGraphs];

}

- (void)refreshData {

    __block NSString *labelString = @"";
    __block NSString *unitString = @"";

    NSCalendar *calendar = [NSCalendar currentCalendar];
    NSDate *startDate = [calendar startOfDayForDate:[NSDate date]];
    NSDate *endDate = [calendar dateByAddingUnit:NSCalendarUnitDay value:1 toDate:startDate options:0];
    NSPredicate *predicate = [HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionNone];

    for (NSDictionary *item in sortedDashboardItems) {

        NSString *type = item[@"type"];

        HKSampleType *sampleType = [HKSampleType quantityTypeForIdentifier:type];

        HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:sampleType predicate:predicate limit:HKObjectQueryNoLimit sortDescriptors:nil resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {

            if (!results) {

                NSLog(@"No results were returned form query");

            } else if (error) {

                NSLog(@"Error: %@ %@", error, [error userInfo]);

            } else {

                dispatch_async(dispatch_get_main_queue(), ^{

                    //processing

                    int order = [item[@"order"] intValue];
                    NSNumber *orderNumber = [NSNumber numberWithInt:order];

                    UIFont *arialLarge = [UIFont fontWithName:@"AvenirNext-Bold" size:15.0];
                    UIFont *arialSmall = [UIFont fontWithName:@"AvenirNext-Bold" size:8.0];
                    NSDictionary *arialLargeDict = @{NSFontAttributeName : arialLarge};
                    NSDictionary *arialSmallDict = @{NSFontAttributeName : arialSmall};

                    NSMutableAttributedString *largeString = [[NSMutableAttributedString alloc] initWithString:labelString attributes:arialLargeDict];
                    NSMutableAttributedString *smallString = [[NSMutableAttributedString alloc] initWithString:unitString attributes:arialSmallDict];

                    [largeString appendAttributedString:smallString];

                    [itemData setObject:largeString forKey:orderNumber];

                    [myCollectionView reloadData];
                    [refreshControl endRefreshing];

                });

            }

        }];

        [self.healthStore executeQuery:query];

    }

}

- (void)refreshGraphs {

#if DEBUG
    NSLog(@"graphs");
#endif
    NSCalendar *calendar = [NSCalendar currentCalendar];
    NSDateComponents *interval = [NSDateComponents new];
    interval.day = 1;

    NSDate *anchorDate = [calendar dateByAddingUnit:NSCalendarUnitDay value:-6 toDate:[calendar startOfDayForDate:[NSDate date]] options:0];

    dispatch_queue_t queue = dispatch_queue_create([@"graph.queue" UTF8String], DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group = dispatch_group_create();

    for (NSDictionary *item in sortedDashboardItems) {

        NSMutableArray *arrayOfValues = [NSMutableArray new];

        NSString *type = item[@"type"];

        NSUInteger index = [sortedDashboardItems indexOfObject:item];

        HKQuantityType *quantityType = [HKObjectType quantityTypeForIdentifier:type];

        NSDate *endDate = [NSDate date];

        NSDate *startDate = [calendar dateByAddingUnit:NSCalendarUnitDay value:-6 toDate:[calendar startOfDayForDate:endDate] options:0];

        dispatch_block_t block = ^{

            dispatch_semaphore_t lock = dispatch_semaphore_create(0);

            if ([type isEqualToString:HKQuantityTypeIdentifierBodyMass]) {

                HKStatisticsCollectionQuery *query = [[HKStatisticsCollectionQuery alloc] initWithQuantityType:quantityType quantitySamplePredicate:nil options:HKStatisticsOptionDiscreteAverage anchorDate:anchorDate intervalComponents:interval];

                query.initialResultsHandler = ^(HKStatisticsCollectionQuery *query, HKStatisticsCollection *results, NSError *error) {

                    //processing

                };

                [self.healthStore executeQuery:query];

                dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);

            } else if ([type isEqualToString:HKQuantityTypeIdentifierBodyMassIndex]) {

                HKStatisticsCollectionQuery *query = [[HKStatisticsCollectionQuery alloc] initWithQuantityType:quantityType quantitySamplePredicate:nil options:HKStatisticsOptionDiscreteAverage anchorDate:anchorDate intervalComponents:interval];

                query.initialResultsHandler = ^(HKStatisticsCollectionQuery *query, HKStatisticsCollection *results, NSError *error) {

                    //processing

                };

                [self.healthStore executeQuery:query];

                dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);

            } else if ([type isEqualToString:HKQuantityTypeIdentifierHeartRate]) {

                HKStatisticsCollectionQuery *query = [[HKStatisticsCollectionQuery alloc] initWithQuantityType:quantityType quantitySamplePredicate:nil options:HKStatisticsOptionDiscreteAverage anchorDate:anchorDate intervalComponents:interval];

                query.initialResultsHandler = ^(HKStatisticsCollectionQuery *query, HKStatisticsCollection *results, NSError *error) {

                    //processing

                };

                [self.healthStore executeQuery:query];

                dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);

            } else {

                HKStatisticsCollectionQuery *query = [[HKStatisticsCollectionQuery alloc] initWithQuantityType:quantityType quantitySamplePredicate:nil options:HKStatisticsOptionCumulativeSum anchorDate:anchorDate intervalComponents:interval];

                query.initialResultsHandler = ^(HKStatisticsCollectionQuery *query, HKStatisticsCollection *results, NSError *error) {

                    if (error) {

                        NSLog(@"Error: %@ %@", error, [error userInfo]);

                    } else {

                        [results enumerateStatisticsFromDate:startDate toDate:endDate withBlock:^(HKStatistics *result, BOOL *stop) {

                            HKQuantity *quantity = result.sumQuantity;

                            if (quantity != nil) {

                                //do some processing

                            } else {

                                [arrayOfValues addObject:@0];

                            }

                        }];

                        [self addArrayToGraphData:arrayOfValues atIndex:index];

                        dispatch_semaphore_signal(lock);

                    }

                };

                [self.healthStore executeQuery:query];

                dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);

            };

        };

        dispatch_group_async(group, queue, block);

    }

    dispatch_group_notify(group, queue, ^{

        [myCollectionView reloadData];

    });

}

- (void)addArrayToGraphData:(NSArray *)array atIndex:(NSUInteger)index {

    [graphData replaceObjectAtIndex:index withObject:array];

}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {

    DashboardCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];

    if (cell != nil) {

        for (UIView *subview in cell.subviews) {

            if ([subview isKindOfClass:[PNLineChart class]]) {

                [subview removeFromSuperview];

            }

        }

        NSNumber *indexNumber = [NSNumber numberWithInteger:indexPath.row];
        NSDictionary *item = [sortedDashboardItems objectAtIndex:indexPath.row];
        NSString *imageSafeString = [item[@"title"] stringByReplacingOccurrencesOfString:@" " withString:@""];
        NSString *imageString = @"";

        if ([layoutType isEqualToString:@"listView"]) {

            imageString = [NSString stringWithFormat:@"%@-large.png", imageSafeString];
            cell.layoutType = layoutType;
            cell.titleLabel.text = item[@"title"];
            cell.dataImage.image = [UIImage imageNamed:imageString];

        } else if ([layoutType isEqualToString:@"gridView"]) {

            imageString = [NSString stringWithFormat:@"%@-small.png", imageSafeString];
            cell.layoutType = layoutType;
            cell.titleLabel.text = @"";
            cell.dataImage.image = [UIImage imageNamed:imageString];

        }

        PNLineChart *graph = nil;

        if ([layoutType isEqualToString:@"listView"]) {

            graph = [[PNLineChart alloc] initWithFrame:CGRectMake(80, 50, self.view.frame.size.width - 100, 90)];

        } else {

            graph = [[PNLineChart alloc] initWithFrame:CGRectMake(10, 0, (self.view.frame.size.width / 2) - 50, 110)];

        }

        graph.showLabel = NO;
        graph.showGenYLabels = NO;
        graph.backgroundColor = [UIColor clearColor];
        graph.pointColor = [UIColor whiteColor];
        graph.userInteractionEnabled = NO;

        [cell addSubview:graph];

        PNLineChartData *data = [PNLineChartData new];
        data.color = [UIColor colorWithHexString:@"49b1d9"];
        if (![graphData[indexPath.row] isEqual:[NSNull null]]) {
            data.itemCount = [graphData[indexPath.row] count];
        } else {
            data.itemCount = 0;
        }
        data.inflexionPointStyle = PNLineChartPointStyleCircle;
        data.getData = ^(NSUInteger index) {

            CGFloat yValue = [graphData[indexPath.row][index] floatValue];
            return [PNLineChartDataItem dataItemWithY:yValue];

        };

        graph.chartData = @[data];
        [graph strokeChart];

        NSAttributedString *dataString = [itemData objectForKey:indexNumber];

        cell.dataLabel.attributedText = dataString;

    }

    return cell;

}

看起来代码太复杂了,任何人都无法查看它并猜测实际发生了什么。如果你把它分成一些适当的模型或服务层,这样会更好,这样做展示的代码和从 healthkit 获取数据的代码就会分开。

我清楚地看到这里的线程问题,首先是 HKHealthStore 方法的使用,

- (void)requestAuthorizationToShareTypes:(NSSet *)typesToShare
                               readTypes:(NSSet *)typesToRead
                              completion:(void (^)(BOOL success,
                                                   NSError *error))completion 

文档中非常清楚地说明了此方法 运行 和回调是在某个任意队列上调用的,

https://developer.apple.com/library/prerelease/ios/documentation/HealthKit/Reference/HKHealthStore_Class/index.html#//apple_ref/occ/instm/HKHealthStore/requestAuthorizationToShareTypes:readTypes:completion:

HealthKit performs these requests asynchronously. If you call this method with a new data type (a type of data that the user has not previously granted or denied permission for in this app), the system automatically displays the permission form, listing all the requested permissions. After the user has finished responding, this method calls its completion block on a background queue. If the user has already chosen to grant or prohibit access to all of the types specified, the completion is called without prompting the user.

现在,您确定您正在主线程中执行所有 UI 调用吗?我认为您正在执行多个异步查询。所以,在这种情况下 dispatch_semaphore 看起来并不复杂。使用 dispatch_group_enterdispatch_group_leave 来管理并发任务是否更有意义?我没有看到您正在主线程中重新加载 collectionview。这些是我从上面的代码中看到的主要问题。如果您选择一些不同对象的服务代码,您将节省大量时间重构和找出错误。