Migrate data trong CoreData (Xử lý lỗi abort())


Tại sao phải migrate data?

Tôi viết một ứng dụng cho nhân viên điều tra thị trường cửa một công ty marketing có thể cầm iPhone đi khắp nơi nhập dữ liệu. Do kết nối Internet không đảm bảo do đó, tôi sử dụng CoreData để lưu trữ dữ liệu điều tra vào ứng dụng, khi có kết nối Internet, dữ liệu sẽ được đẩy lên web server.

Mọi việc diễn ra tốt đẹp. Ứng dụng được cài đặt trên thiết bị của nhân viên điều tra thị trường bằng In-house Enterprise Distribution.
Khoảng hai tháng sau, khách hàng lại yêu cầu tôi bổ xung thêm một nhiều trường vào ứng dụng điều tra nhưng không được phép xoá bỏ dữ liệu cũ đã nhập.

Trong trường hợp nâng cấp ứng dụng và thay đổi cấu trúc model của CoreData như thế này, bắt buộc tôi phải sử cơ chế Data Migration cho phép di cư (migrate) dữ liệu cũ vào model mới của CoreData.

Khi nào có thể migrate data?

Có vài cấp độ migrate data. Cấp độ dễ nhất là “Lightweight migration”. Lightweight migration chỉ thực hiện thành công giới hạn đối với những thay đổi trong model của CoreData như sau:

  • Simple addition of a new attribute: thêm mới một attribute (thêm mới một cột)
  • Removal of an attribute: bỏ attribute ra khỏi một entity (tương đương bỏ cột)
  • A non-optional attribute becoming optional: chuyển thuộc tính bắt buộc thành không bắt buộc
  • An optional attribute becoming non-optional, and defining a default value
  • Renaming an entity or property: đổi tên entity hoặc property

Đối với trường hợp thay đổi phức tạp khiến cho Lightweight migration không thành công, ứng dụng sẽ báo lỗi “Model of CoreData is not found”.

Các bước để lightweight migrate data

Học lập trình iOS trực tuyến cơ bản
Add Model Version
Tạo một version mới kết thừa từ version cũ
Tạo một version mới kết thừa từ version cũ

Chọn một phiên bản cụ thể của CoreData model

Chọn phiên bản cụ thể của Model
Chọn phiên bản cụ thể của Model

Chỉnh sửa lại đoạn code cấu hình CoreData trong AppDelegate.m: chúng cần sửa trong hàm getter của thuộc tính persistentStoreCoordinator để bổ xung lựa chọn kiểu NSDictionary  @{NSMigratePersistentStoresAutomaticallyOption:@YES, NSInferMappingModelAutomaticallyOption:@YES}

Với lựa chọn này: CoreData sẽ chủ động migrade dữ liệu cũ vào model mới và ánh xạ những attribute bị đổi tên.

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    ...
    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
                                                   configuration:nil
                                                             URL:storeURL
                                                         options:@{NSMigratePersistentStoresAutomaticallyOption:@YES, NSInferMappingModelAutomaticallyOption:@YES}
                                                           error:&error]) {
    ...
}    
    
    return _persistentStoreCoordinator;
}

Thử nghiệm

Các bạn có thể thực hành bằng tạo ra một CoreData model gọi là version gốc, sau đó thêm dữ liệu. Tạo một phiên bản mới của model, chọn chế độ Lightweight migration rồi query dữ liệu cũ, đồng thời thêm dữ liệu mới. Nếu ứng dụng sau khi chỉnh sửa CoreData model mà không bị crash có nghĩa quá trình migration thành công.

Ví dụ mẫu

Ví dụ dự án mẫu demo tính năng Lightweight migration trong CoreData

Advertisements

iOS UITableView : Load data while scrolling


I did a small POC on how to load data automatically while scrolling the table view. Data is loading over network using a rest API. So I have choose a 500px API to load set of photos. They are providing API to load featured photos and that has lot of data. So they have providing data as pages. (Which is exactly what I wanted.)

We should have a array to keep loaded data in the memory. I have named this array as photos. This will be the data source for the UITableView. We are dynamically updating this array after each successful data retrieval. So it should be mutable.

@property (nonatomic, strong) NSMutableArray *photos;

Keep private properties to keep track of current page and total number of pages. These two variables will be use to decide whether application should make a request to load more data from the server. 500px API is returning totalNumber of items in their API. For the approach I am using currentPage variable and totalPages will be enough. But for more safety I will add another variable to keep track of totalItems as well.

@property (nonatomic, assign) NSInteger currentPage;
@property (nonatomic, assign) NSInteger totalPages;
@property (nonatomic, assign) NSInteger totalItems;

To load data, I wrote a separate method, which will accept page parameter. After a successful response, photos array will be updated with new elements. In addition to that currentPage, totalPages and totalItems properties will also updated / reset according to the server response. Usually updating the value once is sufficient. But I am expecting two advantages with approach.

  • If the system updating frequently, new data will be available at any time. So problem of not having updated data will avoid with this.
  • Sometimes because of programming errors values of above properties will take wrong values. So we can be confident about having correct page numbers.

Finally we are reload data in the table view.

- (void)loadPhotos:(NSInteger)page {
    
    NSString *apiURL = [NSString stringWithFormat:@"https://api.500px.com/v1/photos?feature=editors&page=%ld&consumer_key=%@",(long)page,kConsumerKey];
    
    NSURLSession *session = [NSURLSession sharedSession];
    [[session dataTaskWithURL:[NSURL URLWithString:apiURL]
            completionHandler:^(NSData *data,
                                NSURLResponse *response,
                                NSError *error) {
                
                if (!error) {
                    
                    NSError *jsonError = nil;
                    NSMutableDictionary *jsonObject = (NSMutableDictionary *)[NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&jsonError];
                    
                    [self.photos addObjectsFromArray:[jsonObject objectForKey:@"photos"]];
                    
                    self.currentPage = [[jsonObject objectForKey:@"current_page"] integerValue];
                    self.totalPages  = [[jsonObject objectForKey:@"total_pages"] integerValue];
                    self.totalItems  = [[jsonObject objectForKey:@"total_items"] integerValue];
                    
                    dispatch_async(dispatch_get_main_queue(), ^{
                        [self.tableView reloadData];
                    });
                }
            }] resume];
}

Now the things about the UITableView. When deciding number of rows, we should first check whether there are more data to load. If the current page is equal to totalNumber pages that means we are done. We loaded all the available data from the server. For more safety we can check for totalItems with the size of the photos array. If current page is not equal to total number of pages, We increment the numer of rows by 1 to show a additional cell with loading activity indicator.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    if (self.currentPage == self.totalPages
        || self.totalItems == self.photos.count) {
        return self.photos.count;
    }
    return self.photos.count + 1;
}

In tableView:willDisplayCell:forRowAtIndexPath: method, we checking whether the cell which going to display is last cell. That’s the cell with the loading activity indicator. If so we are making a server call to load data for next page.

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.row == [self.photos count] - 1 ) {
        [self loadPhotos:++self.currentPage];
    }
}

Here’s body for tableView:cellForRowAtIndexPath: method. If the cell is last cell, we are first showing the loading indicator. Otherwise we are constructing the cell with current element of the photos array. Here you’ll see a strage method which has a sd_ prefix. This is not a standard method of UIImageView class. I did a small improvement to cache images locally using SDWebImage library.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    UITableViewCell *cell = nil;
    
    if (indexPath.row == [self.photos count]) {
    
        cell = [tableView dequeueReusableCellWithIdentifier:@"LoadingCell" forIndexPath:indexPath];
        UIActivityIndicatorView *activityIndicator = (UIActivityIndicatorView *)[cell.contentView viewWithTag:100];
        [activityIndicator startAnimating];
        
    } else {
        
        cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
    
        NSDictionary *photoItem = self.photos[indexPath.row];
        cell.textLabel.text = [photoItem objectForKey:@"name"];
        if (![[photoItem objectForKey:@"description"] isEqual:[NSNull null]]) {
            cell.detailTextLabel.text = [photoItem objectForKey:@"description"];
        }
        
        [cell.imageView sd_setImageWithURL:[NSURL URLWithString:[photoItem objectForKey:@"image_url"]]
                          placeholderImage:[UIImage imageNamed:@"placeholder.jpg"]
                                 completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
                                     if (error) {
                                         NSLog(@"Error occured : %@", [error description]);
                                     }
        }];
    }
    
    return cell;
}

Server API considerations.

If your API has lot of data to return, then I suggest to use pagination. Without just returning data for requested page, it’ll be helpful to developers to if the API is returning current page, totoal pages, total items count with the response. These information will be change according to the data you have. But without maintaining lot of data locally inside phone / browser we can use server if those are included in response.

Some APIs returning only total items without returning the total number of pages. For such occasions we can calculate the total number of pages using following method. Source.

totalPages = (totalCount + pageSize - 1) / pageSize;

XCode project source code

Working with a Static Library/Framework vs Embedded Framework


Terminology to simplify discussion —
Static Library = Static Framework
Embedded frameworks = Dynamic frameworks
You can find standard defnitions and details about Static library vs Dynamic linker here.

Putting forward my exprience with embedded frameworks and then to support iOS7 having to move back to static library sharing all the useful links and resources.

Recently for a project I was fluctuating between using a static library or an embedded framework for a project which supported iOS7+ users.I had not worked on a static library till then and there was Dynamic/Embedded frameworks in iOS8.Dynamic or embedded frameworks were introduced in mac but in iOS, only Apple could create dynamic frameworks (UIKit.framework etc).Finally they allowed 3rd party developers to create and ship embedded frameworks in iOS8. Also with the advent of extensions need for code sharing across targets/apps is essential now.

To start sharing code between app and extension using embedded frameworks is fairly easy.

  1. Click on ‘+’ button to add a new target.
  2. Choose “Cocoa Touch Framework”
  3. Your framework is automagically connected to main app target.You don’t need to do anything there.
  4. To connect it to extension targets – 1) Select the extension target.
    2) Go to general tab and click the ‘+’ button in “Linked Frameworks and Libraries” and select the framework.Done.
  5. Also to limit your embedded framework to use APIs not available to extensions you can tick mark “Allo app extension API only” in embedded framework target under Generals tab.This will prevent any APIs (like UIApplication) not available in extensions to come in embedded frameworks.

Following images below depict the steps —

Thats it!! to start sharing your code between App and extensions using embedded frameworks.

  1. To import files from extension use normal import statements with double quotes.
  2. To import storyboard use storyboardWithName:bundle: API.
  3. To import localized strings use NSLocalizedStingFromTableInBundle.
  4. To import images from xcassets use (new iOS8 API) in UIImage — imageNamed:inBundle:compatibleTraitCollection:

Everything looks good but bummer!!

Although the app works fine in iOS7 and above but with a warning “dylibs work with iOS8 and above”.Apple document titled “Deploying a Containing App to Older Versions of iOS” raised my hopes but I didnt understand how to make it work.Most of the blogs, stackoverflow pointed that it wont be accepted by itunes connect or it will be rejected in review.Finally came the reply from Apple folks “Due to some underlying changes in OS embedded frameworks cant work in iOS7.You can support iOS7 for containing app and load embedded framework conditionally in iOS8 or go for a static library.

That means I had to move back to static library.Here is the Raywenderlich tutorial to get started wth static library. Here are some of the things which turned out to be challening while working with static library —

Usually your static library would consist of following parts —

  1. Code— All import statements needs to be done using angular bracket syntax.For instance #import <>.
  2. Assets — Storyboard,Xibs, Assets, Strings — You can’t put resources in a static library.So the only option is to attach a separate bundle containing assets, storyboards, strings with the static library.Here is the articlewhich explains it properly.

All strings,storyboards in static library bundle can be accessed just like in a embedded framework but for UIImage there is no method of “imageNamed:” variant to load image from bundle in iOS7.(iOS8 provides one as mentioned above.) So you have to use “imageWithContentsOfFile” loading appropriate image file in iOS7 to load images from xcassets of bundle.

There you go your static library is up and running and you can successfully share code among your apps and extensions.

References —

  1. http://www.raywenderlich.com/41377/creating-a-static-library-in-ios-tutorial#comments
  2. Static library with resources — http://www.galloway.me.uk/tutorials/ios-library-with-resources/
  3. http://www.raywenderlich.com/65964/create-a-framework-for-ios
  4. http://www.cocoanetics.com/2011/12/sub-projects-in-xcode/
  5. http://www.cocoanetics.com/2012/01/helping-xcode-find-library-headers/
  6. http://stackoverflow.com/questions/5543854/xcode-4-cant-locate-public-header-files-from-static-library-dependency

Do put forward your views about the article.My Email is cocoagarage.co@gmail.com and twitter handle @cocoagarage.

iOS Library With Resources


The other day I was finding myself wondering why it was so complicated to create a “framework” for iOS which contained XIBs, graphics, etc. People are using techniques such as iOS Universal Framework which I tried but had problems with setting a breakpoint in the framework project. I thought there must be a better way to do it when I thought about the idea of having a simple static library and a resources bundle. It worked! And in this tutorial I show how you can do it for yourself.

If you want to just skip to download the sample project then please do so, or carry on reading for a full description of what to do.

Step 1: Create the library project

First of all you need to create a library project which will be a standard Cocoa Touch static library first of all. So go ahead and create a new project from the “Cocoa Touch Static Library” template as shown in the following two screenshots.

Create library project A

Create library project B

Step 2: Adding a copy files phase

In order for the headers from the static library to be picked up by a project which includes it, we need to add a copy files phase to the library to copy the headers to a certain directory. From a bit of trial and error and some other blog posts I found that we need to put these into the “Products Directory” under a subpath of include.

To add the phase you need to go to the build settings of the library project and under the “Build Phases” tab you’ll see a button called “Add Build Phase”. Click that and set it up as per the screenshots below.

Add copy files stage

Copy files stage

Step 3: Adding the resources bundle target

The magic for getting the resources to play nicely is to put all the resources into a bundle which we’ll include from another project. So we need to add a target to the library project of type “Bundle”. From the build settings of your library project click the “Add Target” button and use the screenshot below to help you set it up.

Add bundle target

Step 4: Fixing the resources bundle target

When the resources bundle target is added, it will default to being set up with a Mac OS X build target. This is wrong because we want an iOS one so you just need to change the relevant settings to whatever you want your iOS target to be (probably “Latest iOS”). Use the screenshot below to guide you in doing this.

Edit bundle target

Step 5: Adding a XIB to the bundle target

In order to get resources to be put into the bundle all you have to do is add them to the “Copy Bundle Resources” build phase of the target. The following screenshot shows an example of MyViewController.xib being put into the bundle.

Add XIB to bundle target

Step 6: Creating the app project

Now we’re ready to create an app which will use the library. So let’s go ahead and do that. Just create an empty application for now. The following screenshots show an example of doing that.

Create app project A

Create app project B

Step 7: Linking with the library

In order to get the static library to be linked with the main app project you just need to drag the library project from Finder into the navigator pane in Xcode and drop it next to the project. It doesn’t really matter if it’s a sub project or a sibling project in a workspace – do whichever you feel most comfortable with.

Then we need to edit the app’s scheme to make it build the library project’s targets first. So edit the scheme and under the “Build” tab click the plus at the bottom and add the library target and library resources target. You should then have something which looks like the following screenshot.

Edit app scheme

Step 8: Linking against the library

We now need to tell the app project to link against the static library from the library project. To do this you just need to add it to the list of linked frameworks in the target. The following screenshot shows an example of doing this.

Add library to frameworks

We also need to add the bundle from the library project to the main app project. To do this we just need to drag it across from the navigator pane to the “Copy Bundle Resources” phase of the application project. Once done it should look something like the following screenshot.

Drag bundle to resources

Step 9: Final setup of app project

The last bit to do is to set up the header search paths of the app project. Go to the build settings of the app project and look for “User Header Search Paths”. Then set the target setting to $(BUILT_PRODUCTS_DIR) so that it looks something like the following screenshot.

Edit header search path

After you’ve done that you’re ready to go! The finished project should look something like the following screenshot in the navigator pane.

Final project

Step 10: Coding the example view controller

In my example I created a view controller called MyViewController with a XIB that gets put into the library bundle. The code for this just has to be a bit different to normal to pick up the XIB from our library bundle. So here is the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import <UIKit/UIKit.h>

@interface MyViewController : UIViewController

@end

@implementation MyViewController

- (id)init {
    NSBundle *bundle = [NSBundle bundleWithURL:[[NSBundle mainBundle] URLForResource:@"MyLibraryResources" withExtension:@"bundle"]];
    if ((self = [super initWithNibName:@"MyViewController" bundle:bundle])) {
    }
    return self;
}

@end

The only difference here is that we’re picking the XIB from the MyLibraryResources bundle that is built in the library project. It really is as simple as that. Graphics which are included in that bundle will be immediately accessible from the XIBs included in the library bundle as well.

Here’s an example screenshot just to show it really does work:

App running

Download sample project.

Extras

I found it useful to add a category on NSBundle which enables me to access the library bundle without having to type a lot of code each time. So something like this would suffice:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface NSBundle (MyLibrary)
+ (NSBundle*)myLibraryResourcesBundle;
@end

@implementation NSBundle (MyLibrary)

+ (NSBundle*)myLibraryResourcesBundle {
    static dispatch_once_t onceToken;
    static NSBundle *myLibraryResourcesBundle = nil;
    dispatch_once(&onceToken, ^{
        myLibraryResourcesBundle = [NSBundle bundleWithURL:[[NSBundle mainBundle] URLForResource:@"MyLibraryResources" withExtension:@"bundle"]];
    });
    return myLibraryResourcesBundle;
}

@end

Then you can change the code in your view controller initialisers to be:

1
2
if ((self = [super initWithNibName:@"MyViewController" bundle:[NSBundle myLibraryResourcesBundle]])) {
}

Another useful trick is to have a category on UIImage so that loading images from the bundle is easy. I use something like the following which first looks for the image using the normal imageNamed: method and if that fails then it looks it up in the resources bundle. This is useful because then the app can override graphics as it wishes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@implementation UIImage (MyLibrary)
+ (UIImage*)myLibraryImageNamed:(NSString*)name;
@end

@implementation UIImage (MyLibrary)

+ (UIImage*)myLibraryImageNamed:(NSString*)name {
    UIImage *imageFromMainBundle = [UIImage imageNamed:name];
    if (imageFromMainBundle) {
        return imageFromMainBundle;
    }

    UIImage *imageFromMyLibraryBundle = [UIImage imageWithContentsOfFile:[[[NSBundle myLibraryResourcesBundle] resourcePath] stringByAppendingPathComponent:name]];
    return imageFromMyLibraryBundle;
}

@end

Thủ thuật lập trình IOS


Một số link hay:

http://dreamwaygroup.blogspot.com/2013/11/cai-thien-lap-trinh-ios-1-su-dung-arc-e.html

Xử lý Memory Leaks trong ứng dụng iOS : Phần 1 – Một số kinh nghiệm truy vết


Memory Leaks đã từng là nỗi ám ảnh của rất nhiều lập trình viên iOS, thậm chí cả những lập trình viên nhiều kinh nghiệm trước khi Apple hổ trợ tính năng ARC (Automatic Reference Counting) trong trình biên dịch Objective C.

Thuật ngữ memory leaks chỉ những vấn đề liên quan đến memory của ứng dụng, bao gồm việc memory không ngừng tăng trong quá trình sử dụng ứng dụng còn gọi làunbounded growth memory, hoặc có những vùng nhớ tồn tại nhưng không hề có một tham chiếu nào đến nó và không bao giờ bị hủy.  Ngoài hai vấn đề này, tôi cũng sẽ nói đến lỗi phổ biến EXC_BAD_ACCESS trong ứng dụng iOS và một số kinh nghiệm fix lỗi này.

Trước tiên,  tôi muốn trình bày một số kinh nghiệm để phòng tránh memory leaks. Nếu bạn đang không sử dụng trình biên dịch hổ trợ ARC trong dự án của bạn, do yêu cầu hoặc do bạn phải maintain một dự án trước đó không sử dụng ARC, cho dù là do cái gì thì bạn đang có nguy cơ gặp memory leaks, do vậy hãy ghi nhớ thật kỷ 4 điều sau:

  • Đừng bao giờ sử dụng autorelease nếu bạn biết có thể release đối tượng mà bạn đã tạo ra.
  • Chỉ release đối tượng khi chính bạn là người tạo ra đối tượng đó bằng việc sử dụng các hàm: alloc, retain, archive, unarchive.
  • Luôn luôn code cặp lệnh alloc và release liền nhau , sau đó chèn những mã lệnh khác vào giữa cặp lệnh đó. Kết quả của style coding này là bắt đầu mỗi phương thức của bạn luôn là mã lệnh khởi tạo (alloc) đối tượng, và kết thúc mỗi phương thức luôn là mã lệnh release đối tượng. Do vậy bạn sẽ không bao giờ sót việc gọi release một đối tượng do chính bạn tạo ra.
  • Hãy suy nghĩ cẩn thận trước khi bạn retain một đối tượng, và luôn luôn có 1 dòng comment kèm theo ghi chú nơi mà đối tượng đó sẽ bị release hoàn toàn.

Sau 4 điều này, bạn phải luôn luôn chạy Build and Analyze trước Run ứng dụng trên Simulator,  Xcode sẽ phân tích toàn bộ source của bạn, và sẽ báo những dòng code có khả năng gây ra leak memory, hoặc những đối tượng được tạo ra mà không sử dụng, hoặc không được release.

Đến đây xem như bạn đã giảm được một nữa nguy cơ gặp leak memory. Một nữa còn lại, là lúc bạn phải chập nhận tìm và truy vết nó. Một công cụ hữu dụng giúp bạn trong trường hợp này là Leak Instrument tool, hãy vào menu tool Run\Run with Performance Tool\Leaks. Công cụ này sẽ khởi chạy ứng dụng của bạn, qua đó đo các thông số về kích thước vùng nhớ được tạo ra, vùng nhớ nào bị leak, và dòng code nào đã gây ra leak vùng nhớ đó,  thậm chí bạn có thể double-click để đi đến đoạn code đó và fix nó.

Tôi sẽ giới thiệu chi tiết về Instrument  tool ở phần 2 của bài viết này.

Một vấn đề khác liên quan đến memory là crash ứng dụng,  lỗi thường gặp là EXC_BAD_ACCESS,lỗi này là do một đối tượng trỏ đến một vùng nhớ đã bị hủy trước đó, bạn thường chỉ thấy được thông điệp hậu quả chung chung nếu bạn đang debug ứng dụng với Xcode IDE, và sẽ không biệt cụ thể dòng code nào, đối tượng nào đang truy cập vùng nhớ bị hủy. Để thấy được những thông tin đó, bạn cần set tham số NSZombieEnabled = YES trong project settings như hình bên dưới đây

https://i0.wp.com/42games.net/wp-content/uploads/2011/03/Screen-Shot-2011-03-25-at-2.55.48-PM.png

Thiết lập này sẽ giúp bạn thấy được những thông báo cụ thể hơn trong debug console của Xcode IDE về đối tượng truy cập và hàm nào đang gọi khi gặp lỗi EXC_BAD_ACCESS.

Có một số trường hợp, thông số thiết lập này không giúp đưa ra thông tin cụ thể nào cả, ngoài việc thông báo lỗi EXC_BAD_ACCESS, đây là lúc bạn phải thực hiện khoanh vùng và xác định đoạn code đã gây ra lỗi đó, bằng cách comment từng phần cho đến khi nào ứng dụng của bạn hoạt động lại một cách bình thường, như vậy bạn sẽ xác định được đoạn code gây ra lỗi sẽ nằm đâu đó trong phần code đã bị comment lại.

Trong phần 2, tôi sẽ giới thiệu cách sử dụng Instrument tool để xác định vùng nhớ bị leak và cách fix leak memory.  Hẹn gặp lại bạn trong phần 2 của bài viết này.

Lập trình đa nhiệm trên iOS với NSThread, GCD và NSOperation


Bài này tôi sẽ giới thiệu với các bạn những khái niệm cơ bản về lập trình đa nhiệm với NSThread, GCD và NSOperation. Một ứng dụng luôn luôn tồn tại một tiểu trình (thread) chính thực thi ứng dụng đó, việc thực thi bao gồm những xử lý vể giao diện, đọc dữ liệu, tính toán, tất cả code bạn viết mặc định được thực thi trong tiểu trình chính của ứng dụng, còn gọi làmain thread.

Mọi thứ sẽ không vấn đề gì cho đến khi bạn phải thực hiện việc tải nhiều hình từ server, hay phải import dữ liệu từ file CSV hoặc XML có kích thước vài trăm ngàn records để hiển thị, bạn vẫn code được bình thường, và mọi thứ vẫn chạy trên main thread, nhưng lúc này những tương tác người dùng với ứng dụng của bạn không còn mượt mà như trước nữa.  Nguyên nhân là main thread phải xử lý những tác vụ tải, import dữ liệu tốn nhiều thời gian để hoàn thành trước khi kết xuất được kết quả cho người dùng, trong thời gian chờ main thread xử lý xong, người dùng không thể làm gì khác ngoài việc ngồi nhìn ứng dụng của bạn, touch một cách vô vọng, sau đó thì terminate app, Oos oh! No!.

Người dùng không hề muốn touch lên một cái button import để rồi chờ 1-2 giây sau mới thấy kết quả, tôi cũng vậy. Giải pháp cho trường hợp này là bạn phải tạo một tiểu trình (thread) mới, độc lập vớimain thread để thực thi những việc tải hình hay import dữ liệu mất thời gian đó, cho đến khi nào mọi việc thực thi xong, bạn mới đưa dữ liệu đọc được qua main thread để hiện thị kết quảĐể làm được điều này, bạn phải sữ dụng kỹ thuật multithreading bằng NSThread, GCD hoặc NSOperation/NSOperationQueue.

Việc tạo ra một thread mới bằng NSThread khá đơn giản:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 1. Tao thread moi de load image
- (void) createThreadToLoadImage
{
   NSString *imageURL = @"http://www.abc.com/example.png";
   NSThread* myThread = [[NSThread alloc] initWithTarget:self
                                                selector:@selector(processLoadingImageInNewThread:)
                                                  object:imageURL];
  [myThread start];
}
// 2. Ham xu ly load image
- (void) processLoadingImageInNewThread:(NSString*) imageURL
{
    NSURL *url = [NSURL urlWithString:imageURL];
    NSData *imageData = [ [NSData alloc] initWithContentsOfURL: url ];
    [self performSelectorOnMainThread:@selector(imageDownloadDidFinish:) withObject:imageData waitUntilDone:NO];
}
// 3. Ham xu ly hien thi image da duoc load xong
- (void) imageDownloadDidFinish:(NSData*) imageData
{
   UIImage *downloadedImage = [UIImage imageWithData:imageData];
   // Hien thi image len image view
   self.imageView.image = downloadedImage;
}

Trong hàm (1), bạn đã tạo ra một thread mới để gọi thực thi hàm xử lý processLoadingImageInNewThread bằng NSThread, khi thread mới được start, nó sẽ chạy song song và bất đồng bộ với main thread, do vậy sẽ không gây ra tình trạng block UI.

Mọi xử lý liên quan đến các thành phần giao diện bắt buộc phải được thực thi trong main thread

Trong hàm (2), xử lý việc load hình từ một remote URL, như tôi đã nói, việc xử lý này đã được đẩy qua một thread khác, không nằm trong main thread,  và một điều quan trọng bạn phải ghi nhớ: “mọi xử lý liên quan đến các thành phần giao diện bắt buộc phải được thực thi trong main thread”, đây là nguyên tắc. Đó là lý do vì sao bạn phải có hàm (3), xử lý việc hiện thị hình đã được load về trong main thread, làm sao bạn biết nó được thực thi trong main thread ? chính vì bạn đã gọi performSelectorOnMainThread trong hàm (2).

Trong hàm (3), như tôi đã nói,  bạn chỉ việc set dữ liệu image lên image view một cách dể dàng. Bạn chú ý, tất cả code xử lý trong hàm (3) bây giờ đang được thực thi trong main thread, không còn trong thread bạn đã tạo ra trước đó nữa.

Đến đây, bạn đã biết cách load một hình từ server bằng việc sử dụng multithreading, đơn giản phải không.

Với kỹ thuật trên,  để load nhiều hình một lúc một cách bất đồng bộ, có bạn đã cải tiến lại hàm (1) như sau:

1
2
3
4
5
6
7
8
9
10
11
// 1A. Tao thread moi de load image
- (void) createThreadToLoadImage:(NSArray*) imageURLs
{
   for( id imageURL in imageURLs)
   {
       NSThread* myThread = [[NSThread alloc] initWithTarget:self
                                                    selector:@selector(processLoadingImageInNewThread:)
                                                      object:imageURL];
       [myThread start];
   }
}

Trong hàm 1A, bạn nghĩ sao nếu mảng imageURLs có 1000 hình,  sẽ có tương ứng 1000 thread được tạo ra. So bad. why ? Bạn nên nhớ số lượng thread luôn tỉ lệ nghịch với năng lực xử lý của CPU, và pin smartphone của bạn. Càng nhiều thread, CPU sẽ càng mệt mỏi, hệ quả là máy sẽ nóng lên, và hao pin rất nhanh. Bạn sẽ làm sao ?

Hãy tưởng tượng mỗi thread là một công nhân làm thuê cho bạn, và bạn phải trả tiền cho điều đó. Giả sử, một công nhân (thread)  thực hiện load 1 hình tốn 1 giây, nếu bạn thuê 1000 công nhân, như vậy sẽ có rất nhiều người ngồi chơi với bạn khi họ làm xong việc. Stupid phải không? tôi biết bạn chỉ thuê 10 công nhân mà thôi, như thế họ sẽ làm việc liên tục một cách hiệu quả cho bạn. Đây chính là cách thức hoạt động của NSOperationQueue.

“Một NSOperationQueue quản lý một danh sách hàng đợi công việc (NSOperation), và một hoặc nhiều thread (công nhân)  để xử lý công việc đó.”

NSOperationQueue tạo ra một hàng đợi, chứa tất cả công việc (NSOperation) mà bạn đưa vào, sau đó phân phát công việc này cho mổi anh công nhân được thuê để xử lý hàng đợi. Thuê bao nhiêu người là do bạn chỉ định. Như vậy, một NSOperationQueue quản lý một danh sách hàng đợi công việc (NSOperation), và và một hoặc nhiều thread (công nhân)  để xử lý công việc đó.

Tất cả những gì bạn phải làm là tạo ra một operation queue, và định nghĩa một công việc (operation) để đưa vào queue. Bạn có thể định nghĩa một công việc bằng cách định nghĩa một lớp XYZOperation kế thừa từ lớp NSOperation, hoặc sữ dụng lớp tiện ích đã được Apple viết sẳn NSInvocationOperation và NSBlockOperation. Dưới đây là một ví dụ sữ dụng NSInvocationOperation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1B. Load image su dung NSOperationQueue
- (void) usingNSOperationQueueToLoadImage:(NSArray*) imageURLs
{
   NSOperationQueue *operationQueue = [NSOperationQueue new];
   [operationQueue setMaxConcurrentOperationCount:10];
   for( id imageURL in imageURLs)
   {
       NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:
                                             selector:@selector(processLoadingImageInNewThread:)
                                               object:imageURL];
      [operationQueue addOperation:operation];
      [operation release];
   }
   [operationQueue start];
}

Trong 1B, bạn đã tạo ra một operation queue, và chỉ định 10 anh công nhân làm việc cho bạn bằng hàm setMaxConcurrentOperationCount, và tạo ra một danh sách các công việc bằng lớp NSInvocationOperation, bạn lưu ý, bạn đã chỉ định công việc phải làm đơn giản bằng cách set selector cho NSInvocationOperation, đây chính là sự đơn giản mà Apple muốn làm giúp bạn với lớp NSInvocationOperation.

Một điều nữa, bạn không cần quan tâm đến việc tạo và quản lý thread nữa, NSOperationQueue đã làm điều đó giúp bạn, tạo thread, tái sữ dụng lại thread đó cho công việc tiếp theo trong hàng đợi khi một thread đã thực thi xong một công việc trước đó. That is! Đây chính là tiện ích của NSOperationQueue, chính là điều Apple muốn làm giúp bạn, tất cả những gì bạn cần làm là định nghĩa công việc cần phải làm mà thôi.

Bạn vẫn chưa nghe tôi nhắc đến GCD (Grand Central Dispatch), GCD là gì và có giúp gì cho bạn như NSOperationQueue đã làm không ? câu trả lời là Có! nhưng với một cách thức hơi khác thường bằng việc sữ dụng block style coding. Tôi sẽ nói về block style coding trong objective c ở một bài khác, tôi sẽ tập trung vào việc xem GCD hổ trợ việc lập trình multithreading như thế nào. Có 3 vấn đề ta cần quan tâm là hàng đợi công việc, tạo thread và chỉ định hàm xử lý  trong thread với GCD như thế nào.

GCD hỗ trợ việc quản lý một hàng đợi công việc như NSOperationQueue. Mặc định, một ứng dụng iOS luôn luôn tồn tại một hàng đợi được tạo sẵn gọi là main queue, và main thread  được tạo ra để xử lý những công việc trong hàng đợi này. Có một hàng đợi khác gọi là global queue là hàng đợi của hệ thống, quản lý thread đang thực thi trên các ứng dụng khác nhau. Bạn cũng có thể tạo ra hàng đợi của chính bạn bằng GCD.

Bạn không phải chỉ định có bao nhiêu thread được phép khởi tạo trong một hàng đợi, điều này dể hiểu đối với main queue và global queue, nhưng ngay cả hàng đợi do chính bạn tạo ra cũng vậy. Việc tạo ra bao nhiêu thread là hiệu quả nhất, iOS sẽ chỉ định điều đó giúp bạn.

Như vậy việc còn lại bạn quan tâm là tạo ra một công việc để GCD đưa vào hàng đợi như thế nào, và làm sao để lấy dữ liệu kết quả khi công việc trong hàng đợi của GCD được xử lý xong. Tôi sẽ cho bạn xem một ví dụ cách thức làm điều đó với GCD:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1C. Load image su dung GCD
- (void) usingGCDToLoadImage:(NSArray*) imageURLs
{
   // C0. Code dang duoc thuc thi tren main thread
   // Tao ra mot hang doi bang GCD
   dispatch_queue_t myQueue = dispatch_queue_create("com.myApp.myQueue", NULL);
   // Dua danh sach cong viec vao hang doi
   for( id imageURL in imageURLs)
   {
      dispatch_async(myQueue,
      ^{
         // C1. Code dang duoc thuc thi trong thread cua hang doi do ban tao ra, khong phai la main thread
         NSURL *url = [NSURL urlWithString:imageURL];
         NSData *imageData = [ [NSData alloc] initWithContentsOfURL: url ];
         // Dua ket qua ve main thread de xu ly hien thi UI
         dispatch_async(dispatch_get_main_queue(),
         ^{
              // C2. Code dang duoc thuc thi trong main thread
              UIImage *downloadedImage = [UIImage imageWithData:imageData];
              self.imageView.image = downloadedImage;
          });
       }
}

Trong 1C, đầu tiên bạo tạo 1 hàng đợi của chính bạn như cách bạn tạo ra một NSOperatioQueue dùng hàm dispatch_queue_create. Tiếp theo  bạn đưa danh sách công việc vào hàng đợi này.

Bạn để ý hàm dispatch_async(…), hàm này được truyền vào 2 tham số, tham số đầu tiên là queue mà bạn đã tạo ra ở bước 1, tham số thứ 2 là một hàm xử lý được định nghĩa dưới dạng block, một block xử lý được định nghĩa trong dấu ^{…}, hơi khác thường nếu bạn chưa làm việc với block bao giờ, nhưng đừng quan tâm nó. Bạn chỉ cần hiểu, bạn đang truyền vào tham số cho hàmdispatch_async là một hàm xử lý (hay đúng hơn một con trỏ hàm), hàm này sẽ được triệu gọi trong xử lý của hàm dispatch_async.

Bạn chú ý, tùy vào queue mà bạn truyền vào hàm dispatch_async mà đoạn code block xử lý ở tham số thứ 2 sẽ chạy trên những thread khác nhau, nếu bạn truyền vào main queue thì code xử lý của bạn sẽ chạy trên main thread, và ngược lại.

Ở đoạn code trên, đầu tiên bạn đã truyền vào 1 queue do chính bạn định nghĩa, nên GDC sẽ tự tạo quản lý (tạo hoặc tái sử dụng) 1 thread khác không phải là main thread để thực thi đoạn code C1 của bạn. Khi C1 được xử lý xong, kết quả phải được đưa về main thread để hiện thị. Như tôi đã nói, bạn chỉ việc gọi lại hàm dispatch_async và đưa vào main queue, bạn lấy main queue của ứng dụng bằng hàm dispatch_get_main_queue được hổ trợ bởi GCD, như vậy đoạn code C2 của bạn sẽ được thực thi trên main thread.

Trong một hàm usingGCDToLoadImage nhưng lại có 2 đoạn code khác nhau chạy trên 2 thread khác nhau. Đây chính là điểm khác thường khi bạn sữ dụng GCD với block style coding, nhưng tôi chắc đến đây bạn đã hiểu vì sao, và bây giờ đó là điều bình thường thôi.

Về tính năng thì GCD và NSOperationQueue có chung một mục đích, từ iOS 4 trở về trước, thì GCD và NSOperationQueue được implement độc lập nhau, nhưng từ iOS 4 trở về sau này, NSOperationQueue được implement lại dựa trên GCD.

Chắc chắn bây giờ, bạn sẽ muốn quên đi NSThread, và lựa chọn sữ dụng NSOperationQueue hoặc GCD. Điều đó là tùy thuộc vào bạn, nhưng hãy suy nghĩ xem tại sao bạn dùng GCD mà không dùng NSOperationQueue và ngược lại. Suy nghĩ nhé. Tôi phân tích cùng bạn ở bài sau.

Have a good day !