Abandoned Memory ?

흔히 얘기하는 Leaked Memory 는 reference가 존재 하지 않는 잃어버린 Memory를 이야기한다. 

그럼 Abandoned Memory는 무엇일까?

Abandoned Memory는 Reference가 존재하지만 사용하지 않는 Memory 이다.

Abandoned Memory
는 Profiler가 찾을 수 없기 때문에, Debugging하기가 매우 어렵다.
허나, Instrument 의 Heap Shot기능을 이용하면,
비교적 쉽게 Debugging이 가능하다. Abandoned Memory를 찾아 낼수 있다.

Heapshot의 기능? 

Heapshot의 기능은 Heapshot간의 Memory Allocation Diff 정보를 제공한다는것이다.

HeapShot#0 -> Do some task -> HeapShot#1

이런식으로 흘러 갔을때, HeapShot#0와 HeapShot#1의 Memory allocation Diff가 0 bytes가 되는것이 이상적이라 할수 있겠다.

현실에선, Cache등의 이유로 Diff가 0가 되는일은 거의 없겠다.

허나 HeapShot이 Diff를 제공하기에 Task 후에 살아 남은 Memory Block들을 한눈에 볼수 있고,
내가 의도지하지 않은 Memory Block을 비교적 쉽게 찾아 낼 수 있다.

HeapShot 예제


위의 Sample Program은 2 Button이 존재하며, Memory Leak을 유발하는 Button 하나와 아무것도 하지 않느 버튼 하나가 존재한다.

-(IBAction) pressLeak:(id)aSender {

    [[NSArray alloc] initWithObjects:@"abcdef",@"abcdef",@"abcdef",@"abcdef", nil];

}


Leak Button을 Touch 하면 아무의미 없이 NSArray Object하나를 만든다. 


Profile 모드로 앱을 실행시키고, Instrument Template은 "Allocation"을 선택한다.

좌측 메뉴의 MarkHeap을 눌러 Heap shot을 찍을 수 있다.

아래 그림은 Heapshot을 찍은후의 Heapshot List이다.


Heap Growth Column이 Heapshot을 찍었을 당시의 이전 HeapShot과의 Memory Diff라고 보면 된다.
Memory Leak을 보여주는것이 아니라, Memory Allocation Diff를 보여주는것이기때문에,
Heapshot에 기록되는 모든 Memory가 Leak 혹은 Abandoned 되었다고 판별할 수 없다.

필자는 Heapshot4와 Heapshot8을 찍기 직전에 "Leak" button을 Touch 했다.

 
Heapshot8을 펼쳐 보면 예상과의 달리 Object들이 많이 생성되었다.
아마 Touch Handling을 위해 Framework단에서 생성하는 Object들일것이다.

위의 Test Code를 보면 NSArray를 만들었기에,  유일한 NSArray Object의 생성주기를 보자.(Instrument 우측 View에 나타난다.)


우측에 보면, Object가 만들어진 Stack Trace가 노출되고, Framework 코드가 아닌, 내가 작성한 코드는 Bold로 Highlight된다.
해당 부분을 Double Click 하면 코드도 볼수 있다.


생각보다 Smart한 방법은 아니고, 사람에 의존적인 방식이다.
허나 Diff를 제공하여, 살아 남은 Object 를 추적할 수 있기에, 매우 유용하다.

Memory 문제에 자유로운 개발자는 아마 없을터,
Leaked Memory가 아니라, Abandoned Memory 를 찾아 내는데는  꽤나 유용한 Tip이라고 생각한다.
 

요새 CoreData 관련 공부에 몰두 하고 있습니다.
이런저런것들을 보고 있는데, CoreData Version Up과 LightWeight Migration에 관련 챕터를보았는데,

Modeling 을 변경했을때 자동으로 Data Migration을 해준다는것입니다 ! 
 
그래서 한번 해보기로 했습니다.

아래는 WWDC 2010 의 "Mastering CoreData" Session의 Slide 캡처 화면입니다.

 
음 샬라샬라~
기존의 모델을 유지하고, Version  올려 주고, Option만 주면 된다는 간략한 내용이군요

기존의 자료 ( Apple의 Core Data Programming Guide 마저도... ) Xcode3기준으로 작성되어 약간의 Editor 사용법이 다르지만, 어렵지는 않습니다.
진행 순서는 아래와 같습니다.

1. 버전 추가
2. Attribute renaming
3. 소스 변경
4. Model Current Version 변경
5. 옵션 추가

1. 버전 추가

 위와 같이 Model Editor에서 Editor->Add Model Version 클릭 해주시면 Version 생성 다이얼로그가 나오고는데 적당한 이름을 사용하시면 됩니다.

2.  Attribute renaming
attribute의 이름을 변경합니다. 저는 createdDate라는 Attribute를 createdDate2로 변경했습니다.
예제를 rename 으로 한데는 이유가 있겠죠, 

rename의 경우 original attribute name을 core data 쪽에 알려줘야합니다.

entity를 선택한후 Data Model Inspector의 "Renaming ID"에 원래 이름을 적어줍니다 저 같은 경우 createdDate가 되겠군요


3. Class 변경

class 의 변수 이름을 createdDate 에서 createdDate2로 변경해줍니다.
Xcode내의 refactoring 기능을 이용하면 한번에 수정가능합니다.

단, NSSortDescriptor나 NSPredicate에서 문자열로 사용한 경우에는 수동으로 찾아서 변경해주어야합니다.

 

 4. DataModel 현재 버전 변경하기


위에서 보시는 바와 같이, Version을 추가하여, 새로 만들었지만, 현재는 예전의 Model을 사용하도록 되어 있습니다. 
Model 파일의 Root(Locations.xcdatamodelId)를 선택한후 File Inspector를 열어서 현재 버전을 세팅 하실수 있습니다.

 변경후의 모습은 다음과 같겠군요


 5. NSPersistentStoreCoordinator Open Option설정하기

기본적으로는 Migration기능이 비활성화 되어 있기 때문에, lightweight migration기능을 사용하기 위해선, 몇가지 옵션을 설정해주어야 합니다.

NSInferMappingModelAutomaticallyOption 과 
NSMigratePersistentStoresAutomaticallyOption 입니다.

코드는 아래와 같겠군요

NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES],NSMigratePersistentStoresAutomaticallyOption,
                             [NSNumber numberWithBool:YES],NSInferMappingModelAutomaticallyOption,
                             nil];
 
    NSError *error = nil;
    __persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error])
    {}


 
이렇게 하고 실행하시면, 수정된 Model Class로 이전의 Data까지 사용가능하게 됩니다.

별로 어려운건 아닙니다만, 이래저래 개념적으로 확인해야 할것들이 많군요

CoreData는 살펴볼수록 괜찮은 녀석인것 같습니다 ^^ 

Question 

NSTimer를 걸어 놓고, iOS Background 에 갔다 오면 어떻게 될까?

TestCase 1

fire -> enter background -> enter foreground -> fire

background 갔다 온것에 관계 없이, 똑같이 수행된다. 


TestCase 2
 

fire -> enter background -> fire, fire, fire, fire -> enter foreground

결과 : foreground로 돌아 오는 시점에 4번의 timer selector 가 호출된다. ( 이건 문제다.. )


 

Interface Builder에서 Custom Class 를 사용할때, Unknown Class Error가 발생할 때,

 
이 문제가 발생하는 원인은, Objective-C에서 정적 library를 사용의 고질적인 문제 인데,

static library가 RunTime에 제대로 로드 되지 않아서 이다.

실제로 코드 상의 문제가 있다기 보다는 Objective-C 자체 버그 인 셈이다.

이 문제를 해결하기 위해선

Other Linker Flags , -all_load를 해주면 해결이 된다.

이러면 어플리케이션이 Load될때, Dependency 가 걸려 있는 모든 Static Library를 강제로 로드 한다.
이것말고도 Static Library에서의 Category 등 Side-Effect가 꽤 많다.

이것좀 고쳐 달라고 Apple님들아 :(

 
이 방법은 AppStore에서는 확인해보지 않았습니다. 

1. InfoPlist.string 파일 추가

info.plist 파일을 Localizing해야 할것이라고, 최초에 생각하였지만, 그게 아니었습니다.
info.plist 파일에서 localize 된 값을 위해서는,  NSLocalizedString에서 사용하듯이, InfoPlist.strings파일을 추가 해야 합니다. 




2. Localize 


3. 앱 이름 지정하기 

Info.plist파일에서 AppName을 결정하는것은 "CFBundleDisplayName" 입니다. 

위와 같이 적절하게 값을 채워주고 앱을 실행하면 됩니다 ! ^^


UILabel의 경우, "setTextAlignment:" 로 Text를 정렬한다. 
UIButton에 setTitle:forControlState:를 통해서 Text를 Setting하면, UIButton의 titleLabel에 값이 설정된다.

그래서
[someButton.titleLabel setTextAlignment:UITextAlignmentLeft]; 했는데, 변함이 없다. 

UIButton의 TitleAlignment는

@property(nonatomic) UIControlContentHorizontalAlignment contentHorizontalAlignment; // how to position content hozontally inside control. default is center


UIControlContentHorizontalAlignment를 통해서 해야한다. 


@required

iPhone 3GS or before : 57px 

@optional
iTunesArtWork : 512px - Project에 포함시키지 않아도, App등록시 가능함.
iPhone 4 or later : 114px
Ipad : 72px

Spotlight for iPhone 3GS or before : 29px
Spotlight for iPhone 4 or later : 58px
Spotlight for iPad : 50px

iOS가 점점 지저분해진다. 
Accessibility Inspector는 iOS Simulator에서만 사용이 가능하다. 

설정(Settings) -> 일반(General) -> 손쉬운사용(Accessibility) 에서 설정 가능하다. 


Accessibility Inspector를 사용하면, 위와 같이 나오는데, 현재 Inspecting은 Disable상태이다. 
좌측의 "x" 버튼을 누르면 Inspector View가 펼쳐지면서 활성화가 된다.


Inspector 를 활성화 한 상태에서 "버튼"등의 UI Component를 터치하면, 아래와 같이 정보가 나온다. 


UIAccessibilityProtocol 에 따라 정의 된 값이 아래와 같이 나오면,
프로그래밍 적으로 지정해준적이 없는 경우에는 보통 Default가 나온다. (e.g) UILabel의 꼬리표는 UILabel의 Text가 Default다.)

Voice Over를 지원하는 App을 만들거나 Instruments를 이용한 UI Automation Script를 작성할 때, 유용하게 사용할 수 있다. 


Preface

테스트 자동화에 대한, 필요성, 중요성은 이제 아무리 말해도 입아프다.

Instruments를 이용한 UI Test 자동화는 TDD로 사용하기에는 조금 부족하다. 
Xcode에 Integrated되어 있지 않기 때문에, Test Code와 Real Code간의 전환등이 쉽지 않고, 
Build & Run의 Step이 자동화 되어 있지 않기에 여러모로 불편함이 많다.

개발 부서에서도 당연히 사용하게 되겠지만, QA부서에서의 사용성이 더 부각 된다고나 할까,
여튼 한번 시작해보겠습니다.

UI Automation with Instruments

UIKit 기반 Application용 Test 자동화
Accessibility를 통한 UI Element 접근
Test Script로 JavaScript사용

Instruments에서는 JavaScript를 사용한다. 
JavaScript는 이해하기 쉽고, 널리 알려져 있는 Language다. 즉 그만큼 접근성이 좋다는 뜻이다.

How to automate an Application

1. Automation Template을 이용한 Trace Document 생성


Instruments Template중에 Automation Template을 사용하여 Trace Document를 생성한다. 

2. Target 설정

Target으로는 실제 Device , iOS Simulator둘다 사용할 수 있는데, 이포스트에서는 iOS Simulator를 사용한다. 


"Choose Target"을 선택하여, Test 대상이 될 Application Package(XXXX.app) 을 선택
 Target을 선택한 후 Record 버튼을 누르면 iOS Simulator가 나타남과 동시에 App이 실행된다.

이 포스트에서 사용한 Sample Project는 Apple Developer 사이트 에서 제공하는 Recipes Sample이다.


3. Recipes를 추가하는 Test Script 만들기


Script를 불러와서 Edit는 가능하지만, Create할 수 없다.
적당한곳에서 Test.js파일을 생성하고 불러 온후 "Edit"를 눌러서 Editor를 켠다.


우리가 수행할 Test Phase는 아래와 같다. 

"+" UIBarButtonItem을 터치 
Recipe Name 입력
"Done" UIBarButtonItem 터치
"Recipes" 터치
추가한 Recipe 확인

"+" UIBarButtonITem 터치하기

   UIATarget.localTarget().frontMostApp().mainWindow().navigationBar().buttons()["Add"].tap();

    app.navigationBar().withName("Add Recipe");


Recipe Name 입력

UIATarget.localTarget().frontMostApp().mainWindow().textFields()[0].setValue(name);

"Done" UIBarButtonItem 터치

target.delay(2);

app.navigationBar().rightButton().tap();


"Recipes" 터치

    target.delay(2);

    app.navigationBar().leftButton().tap();


추가한 Recipe 확인

 app.navigationBar().withName("Recipes");

    var cell = app.mainWindow().tableViews()[0].scrollToElementWithPredicate("name beginswith '"+name+"'");

    

    if(cell.isValid() ) {

        UIALogger.logPass(testName);

    }

    else {

        UIALogger.logFail(testName);

    }


TestScript작성법은 다루지 않았는데, 다음 포스트에 할 예정이다. 


"UI Automation Reference" "Instruments User Guide" 을 참고 하면 어렵지 않게 이해할 수 있다. 


4. UI Test




5. Test Result



스크립트 로그 창에, Log Pass/Fail이 표기 된다. 






1. MapView 보여주기

Interface Builder 상에서, MapView를 집어 놓고, Run!! 하면 다음과 같은 에러가 난다. 


그 이유인 즉슨, MapView는 MapKit이라는 Framework에 소속되어 있고, 기본적으로 
MapKit Framework은 링크 되어 있기 때문이다. 


Add Existing Framework을 선택하여, Mapkit 을 추가 해주면. 짜잔하고 잘 나온다. 


2. Annotation Pin 노출하기

- (void)addAnnotation:(id <MKAnnotation>)annotation;


MKMapView에 addAnnotation: Method를 통해 MapView에 Annotation Pin이 표기될 Data를 추가 할 수 있다.

일단 기본적으로 제공하는 MKPlaceMark Object를 생성해서, addAnnotation 해주면, 아래와 같이 Pin이 노출된다.


3. Annotation Draggable 하도록 하기.

// If YES and the underlying id<MKAnnotation> responds to setCoordinate:, 

// the user will be able to drag this annotation view around the map.

@property (nonatomic, getter=isDraggable) BOOL draggable __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_4_0);


MKAnnotationView 의 Header를 살펴보면, draggable 이라는 변수가 있다.
여기서, 주의 할점은 addAnnotation으로 추가한 MKAnnotation을 Adopting하고 있는 Object가
setCoordinate: Message에 응답가능해야 한다는것이다.

우리는 Annotation Object를 MKPlaceMark를 이용했는데, 애석하게도 MKPlaceMark Class는 setCoordinate: Message 에 응답하지 않는다.

@interface DraggableAnnotation : MKPlacemark {


}

@property (nonatomic, readwrite, assign) CLLocationCoordinate2D coordinate;


@end


MKPlacemark를 상속받는 DraggableAnnotation을 정의한다. 
coordinate라는 변수는 MKPlacemark에 이미 존재 함으로 property 만 Override해준다.
그리고 ViewController에서 MKPlacemark를 사용했던 부분을 DraggableAnnotation을 사용하도록 수정한다. 

MKAnnotationView의 draggable값은 기본으로 YES이지만, MKMapView에서 내부적으로 생성하는 MKAnnotationView는 Draggable=NO이다.

MKMapViewDelegate의 Method를 하나 구현한다.

- (MKAnnotationView *)mapView:(MKMapView *)aMapView viewForAnnotation:(id <MKAnnotation>)annotation {

NSString *reuseIdentifier = @"abcdefg";

MKPinAnnotationView *annotationView = (MKPinAnnotationView *)[aMapView dequeueReusableAnnotationViewWithIdentifier:reuseIdentifier];

if(annotationView == nil) {

annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];

annotationView.draggable = YES;

annotationView.canShowCallout = YES;

}

[annotationView setAnnotation:annotation];

return annotationView;

}

 

기본적인 Pin을 보여주는 MKPinAnnotationView Object를 하나 만들고, 
draggable 을 YES로 세팅해주면, 이제 원하는대로 Pin이 Dragging된다. 

MKMapView의 구조가 UIKit이랑은 조금 달르지만, 언제나 그렇듯이 알고 나면 별것 없다. 


+ Recent posts