Android2015. 4. 15. 16:50



  • 객체 생성을 피하라. 객체를 생성하는 작업은 다른 작업들에 비해 로드가 많이 걸리는 작업이며 가비지컬렉션대상이 많아지는것도 성능에 장애요소이다.
  • Native method를 활용하라. 자바코드보다 동일작업시 10~100배 빠르다. 따라서 복잡하고 오래걸리는 연산작업은 native method를 사용하라.
  • 인터페이스 타입참조보다 클래스타입참조를 더 선호하라. 임베디드환경에서는 인터페이스타입참조를 통해 virtual method를 호출하는것이 본래 객체에 대한 class type을 참조하는것보다 2배 느리다.

Map m = new HashMap();  // 인터페이스로 참조하여 참조연산이 더 일어남

HashMap m = new HashMap();  // 명시적으로 참조하므로 추가연산발생안함

  • 가상연결보다 정적연결을 선호하라. 객체필드에 접근할 필요가 없다면 메소드를 static으로 하면 더 빠르다. 가상메서드테이블에 접근하지 않기 때문.
  • 내부에서 getter/setter의 사용은 피하라.
  • 필드참조들을 캐시하라. 멤버변수에 접근하는것이 지역변수에 접근하는것보다 훨씬 속도가 느림. 메소드내에서 멤버젼수에 접근을 많이 하는 경우 로컬변수로 가져와서 처리하면 개이득
  • 상수를 final로 선언하라. static 멤버변수가 초기값을 가지고 있고 값이 변경될 필요가 없다면 final키워드를 사용하면 값을 참조할때 field lookup을 사용하지 않으므로 더 빠르다.
  • Enhanced for loop를 사용하라. for(type n : array) {} 형식의 구문을 말하며 iterator를 사용하는것 보다 일반적으로 빠르다.
  • Enum자료형을 피하라. 코드가독성이나 잘못값을 할당하는 경우를 컴파일시 에러가 발생함으로써 매우 편리하나 코드 크기와 속도면에서는 손해다. 코드사이즈의 크기도 성능에 악영향을 미친다.
  • 내부클래스를 갖는 클래스의 경우 멤버변수를 private대신에 package 접근자를 사용하는것이 좋다. private으로 하면 내부적으로 컴파일러가 접근메서드를 생성하여 해당 메서드를 통해서 접근하기 때문에 접근시마다 함수호출이 발생하여 성능에 영향을 미친다. 

  • Float을 피하라. 일부 모바일 단말의 경우 부동소수점 연산의 지원이 미비할 수 있다.
  • 안드로이드에서 제공하는 개선된 성능의 데이터구조

LruCache

SparseArray

SparseBooleanArray

SparseIntArray

Pair

  • 메인스레드에서 많은 작업을 하지 않아야 한다.

키/터치 이벤트 처리

뷰그리기

라이브사이클 관리

  • 레이아웃 단순 화 및 지연초기화 이용
  • LinearLayout보다는 RelativeLayout이 더 단순한 처리를 하게 됨.
  • ViewStub를 사용해서 객체생성을 지연시킨다.
  • onCreate, onResume등 lifecycle관련 메소드에서는 최소한의 작업만 수행해야 ANR을 피할 수 있으며 사용자의 성능에 대한 민감도를 피해갈 수 있다. 100ms~200ms가 사용지연에 대한 임계치라는 조사가 있다.
  • StrictMode 활용

앱플리케이션 개발동안 StrictMode를 사용함으로써 느려지거나 느려질 가능성이 큰 코드를 검출할 수 있다.

Policy와 penaly를 랩에서 코드레벨로 설정하여 사용가능하다. Policy는 정책을 penalty는 위반시 표시방법을 나타낸다.

Policy는 ThreadPolicy와 VmPolicy 두가지를 설정할 수 있다.

ThreadPolicy: 특정 스레드에 file, network 접근 부분을 감지하여 위반되는 위치를 잡아준다.

VmPolicy : Memory leak의 위치를 감지해 Log, Dialog, DropBox, Splash, 강제종료와 같은 다양한 방식으로 알린다.

ThreadPolicy

detectNetwork() : 네트워크 사용

detectDiskRead() : disk read

detectDiskWrite() : disk write

detectAll() : all

detectCustomSlowCell() : api11이수 StrictMode.noteSlowCall()가 선언된 부분을 감지함.

VmPolicy

detectActiveLeaks() : Activity 서브클래스의 릭감지

detectLeakedClosableObjects(): File/Network IO관련 API사용시 close를 명시적으로 호출안하고 종료할 경우 나오는 리소스 leak감지

setClassInstanceLimit() : 특정클래스가 힘에 생성되는 최대 개 수를 설정하여 object leak현상을 모니터링 할 수 있다.

Panalty

penaltyLog() : logcat으로 표시

panaltyDropBox() : DropBoxManagerService에 로그가 저장되며 정보가 detail하여 추천된다. adb shell dumpsys dropbox data_app_strict_mode —print > strict.txt 로 추출 후 꺼내서 별것을 추천한다.

panaltyDialog() : dialog창으로 표시

penaltyFlashScreen() : 화면상으로 빨간색 테두리의 사각형 splash로 보여준다.

penaltyDeathOnNetwork() : Main스레드에서 network access가 진행되면 무조건 어플리케이션이 강제종료된다. detectNetwork()가 활성화되어 있어야 한다.

Posted by 삼스
Android2015. 4. 13. 18:04


http://developer.android.com/training/displaying-bitmaps/index.html 의 요약임을 밝힌다.

앱사용성에 이슈를 발생시키지 않으면서 메모리제한까지 피할 수 있는 방법에 대해 정리한다. 피하지 않으면 그 유명한 OOM(Out of memory)에 손도 못쓰고 당하게 될것이다.

모바일디바이스는 시스템리소스가 제한적인데 16MB정도의 적은 메모리만이 애플리케이션에 허용되는데 반해 비트맵은 많은 메모리를 사용하게 된다. 그리고 안드로이드 UI는 종종 여러개의 비트맵을 동시에 사용하는 경우가 있다. 이런것들이 어쩔 수 없이 메모리 이슈를 발생시킨다.


아주큰 비트맵을 효율적으로 로딩하기

애플리케이션의 메모리 제한에 문제없이 비트맵을 로딩하는 방법.

이미지를 표시할 UI콤포넌트는 대체적으로 이미지보다 작게 표시된다. 이 때는 이미지 원본사이즈가 아니라 표시될 콤포넌트의 크기에 맞게 로드하는것이 이득이다.

BitmapFactory클래스가 비트맵을 디코딩하기 위한 여러 메소드(decodeByteArray(), decodeFile(), decodeResource())들을 제공한다. 이 메소드들은 비트맵을 디코딩할때 메모리할당을 시도하기 때문에 OOM이 아주 쉽게... 간단하게 발생될수 있다. 이 메소드들에는 BitmapFactory.Options 인자를 받을 수 있는데 inJustDecodeBounds를 true를 주어서 디코딩시 메모리 할당을 피하면서 이미지의 가로/세로 사이즈를 알아낼 수 있다. 이를 이용해서 메모리 할당을 조절할 수 있다.


BitmapFactory.Options options = new BitmapFactory.Options();

options.inJustDecodeBounds = true;

BitmapFactory.decodeResource(getResources(), R.id.myimage, options);

int imageHeight = options.outHeight;

int imageWidth = options.outWidth;

String imageType = options.outMimeType;


인자 스케일다운된 버전을 메모리에 로드해보자

이미지의 사이즈를 알기 때문에 이미지을 풀사이즈를 사용할지 사이즈를 다운할지 결정할 수 있다. BitmapFactory.Options의 inSampleSize를 true로 하며 디코더가 더작은 샘플로 디코딩하도록 할 수 있다.가령 2048x1536사이즈의 이미지를 inSampleSize 4를 주면 512x384사이즈로 로딩한다. 이는 12MB의 메모리 사용량을 0.75MB로 줄이는 효과가 있다.

다음 코드는 inSampleSize를 정하는 코드 예이다.

public static int calculateInSampleSize (BitmapFactory.Options options, int reqWidth, int reqHeight) {

    // Raw height and width of image

    final int height = options.outHeight;

    final int width = options.outWidth;

    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;

        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both

        // height and width larger than the requested height and width.

        while ((halfHeight / inSampleSize) > reqHeight

                && (halfWidth / inSampleSize) > reqWidth) {

            inSampleSize *= 2;

        }

    }

    return inSampleSize;

}


다음은 스케일을 적용한 비트맵로딩 예이다. inJustDecodeBounds를 통해 원본의 크기를 얻은 후 필요한 사이즈에 맞도록 스케일하여 비트맵을 생성하여 반환한다.

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
       
int reqWidth, int reqHeight) {

   
// First decode with inJustDecodeBounds=true to check dimensions
   
final BitmapFactory.Options options = new BitmapFactory.Options();
    options
.inJustDecodeBounds = true;
   
BitmapFactory.decodeResource(res, resId, options);

   
// Calculate inSampleSize
    options
.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

   
// Decode bitmap with inSampleSize set
    options
.inJustDecodeBounds = false;
   
return BitmapFactory.decodeResource(res, resId, options);
}


다음 메소드 호출은 100, 100사이즈에 맞도록 비트맵 이미지를 스케일하여 적용하는 예이다.

mImageView.setImageBitmap(
    decodeSampledBitmapFromResource
(getResources(), R.id.myimage, 100, 100));


비동기로 비트맵 로딩하기


BitmapFactory.decodeXXX 류의 메소드들은 main UI스레드에서 호출되면 안된다. 성능이슈를 발생시킨다. 버벅거리게 될거고 앱 사용자들은 모두 떠날것이다. 

AsyncTask로 백그라운드에서 UI스래드와 분리되어 로딩할 수 있으며 이 때 동시에 수행시 발생하는 이슈에 대해서도 살펴보겠다.

AsyncTask는 별도의 스레드로 어떤 작업을 하고 그 결과를 UI thread에 반환할 수 있도록 해준다. 이 클래스를 extends하고 메소드를 오버라이드하면 되는데 다음 에제를 보자.

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
   
private final WeakReference<ImageView> imageViewReference;
   
private int data = 0;

   
public BitmapWorkerTask(ImageView imageView) {
       
// Use a WeakReference to ensure the ImageView can be garbage collected
        imageViewReference
= new WeakReference<ImageView>(imageView);
   
}

   
// Decode image in background.
   
@Override
   
protected Bitmap doInBackground(Integer... params) {
        data
= params[0];
       
return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
   
}

   
// Once complete, see if ImageView is still around and set bitmap.
   
@Override
   
protected void onPostExecute(Bitmap bitmap) {
       
if (imageViewReference != null && bitmap != null) {
           
final ImageView imageView = imageViewReference.get();
           
if (imageView != null) {
                imageView
.setImageBitmap(bitmap);
           
}
       
}
   
}
}

WeakReference로  ImageView를 처리하는 이유는 해당 메모리릭을 염두에 둔것인데 일반참조를 사용하게 되면 imageView의 참조가 유지되어 GC대상에서 제외될수 있기 때문이다.

** WeakReference로 참조하는 객체는 언제든 메모리에서 해제될수 있기 때문에 사용할 때 get()메소드로 참조를 얻어온 후 null인지 체크한 후 사용해야 한다.

다음 예제는 아주 단순하게 비트맵을 비동기로 새로운 테스크를 만들어서 실행하는 예이다.


public void loadBitmap(int resId, ImageView imageView) {
   
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task
.execute(resId);
}


ListView나 GridView는 childview를 재사용하는 로직으로 구현되어 있다. 이런 경우 AsyncTask로 로딩하게 되면 로딩이 완료되었을 때 view가 사용자의 스크롤로 인해 다른 뷰가 될 수 있다.

이 문제를 해결하기 위해서 Worker task를 갖는 Drawable객체를 사용하게 된다. 여기서는 BitmapDrawable를 사용하여 task가 완료되면 imageView에 이미지를 표시하게 된다.


static class AsyncDrawable extends BitmapDrawable {
   
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;

   
public AsyncDrawable(Resources res, Bitmap bitmap,
           
BitmapWorkerTask bitmapWorkerTask) {
       
super(res, bitmap);
        bitmapWorkerTaskReference
=
           
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
   
}

   
public BitmapWorkerTask getBitmapWorkerTask() {
       
return bitmapWorkerTaskReference.get();
   
}
}


AsyncDrawable은 WorkerTask를 WeakReference로 갖고 있고 getter를 노출하고 있다. BitmapWorkerTask를 실행하기 전에 AsyncDrawable를 생성하여 ImageView에 바인드한다.

public void loadBitmap(int resId, ImageView imageView) {
   
if (cancelPotentialWork(resId, imageView)) {
       
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
       
final AsyncDrawable asyncDrawable =
               
new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
        imageView
.setImageDrawable(asyncDrawable);
        task
.execute(resId);
   
}
}


cancelPotentialWork메소드는 다른 테스크가 이미  imageview에 연관되어 있다면 이전 task를 취소한다. 동일한 테스크로 시도하게 되면 아무것도 하지 않는다.


public static boolean cancelPotentialWork(int data, ImageView imageView) {
   
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

   
if (bitmapWorkerTask != null) {
       
final int bitmapData = bitmapWorkerTask.data;
       
// If bitmapData is not yet set or it differs from the new data
       
if (bitmapData == 0 || bitmapData != data) {
           
// Cancel previous task
            bitmapWorkerTask
.cancel(true);
       
} else {
           
// The same work is already in progress
           
return false;
       
}
   
}
   
// No task associated with the ImageView, or an existing task was cancelled
   
return true;
}


getBitmapWorkerTask는 ImageView에 연결되어 있는 WorkerTask를 반환한다.


private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
   
if (imageView != null) {
       
final Drawable drawable = imageView.getDrawable();
       
if (drawable instanceof AsyncDrawable) {
           
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
           
return asyncDrawable.getBitmapWorkerTask();
       
}
   
}
   
return null;
}


마지막 작업은 onPostExecute에서 task가 취소되었는지 확인하고 ImageView가 유효한지 확인 후 표시한다.


class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
   
...

   
@Override
   
protected void onPostExecute(Bitmap bitmap) {
       
if (isCancelled()) {
            bitmap
= null;
       
}


       
if (imageViewReference != null && bitmap != null) {
           
final ImageView imageView = imageViewReference.get();
           
final BitmapWorkerTask bitmapWorkerTask =
                    getBitmapWorkerTask
(imageView);

           
if (this == bitmapWorkerTask && imageView != null) {
                imageView
.setImageBitmap(bitmap);
           
}
       
}
   
}
}


ListView나 GridView에 아주 적합한 솔루션이다. 또는 child view를 재활용하는 뷰컴포넌트들에서 사용할 수 있는 패턴이다. GridView의 경우 getView()메소드가 테스크와 뷰를 연계해주는 어댑터역할을 한다.


비트맵 캐싱하여 사용하기

아주 많은 이미지를 로딩해야 하며 빈번하게 사용되는 경우라면 캐싱을 고려해야 한다. 메모리캐싱과 파일캐싱이 해당되겠다.

메모리 캐시

메모리를 좀더 사용되겠지만 속도면에서 많은 이득이 있다. LruCache가 비트맵을 캐시하는데 아주 적당하다. LruCache가 사용되기 전에는 SoftReference와 WeakReference를 이용해서 캐싱을 구현하였었는데 이는 Android 2.3의 gc에서 비효율을 야기한다고 한다. 머 무튼 LruCache를 사용하면 나중에 추가한 오브젝트는 LinkedHashMap으로 참조되고 최대개수가 넘어가면 오래된걸 제거하도록 되어 있다.

LruCache의 적절한 사이즈를 결졍하기 위해서 고려해야 할것들이 여러가지가 있겠으나 정해진 크기나 공식은 없다. 전적으로 당신의 분석에 따라서 알아서 결정해야한다. 캐시크기가 크면 메모리가 부족해질거고 캐시크기가 작으면 앱에 로드가 걸릴것이니 적당한 선에서 합의를 해야 함. 참고할만한 가이드는 아래 참고하고

  • How memory intensive is the rest of your activity and/or application?
  • How many images will be on-screen at once? How many need to be available ready to come on-screen?
  • What is the screen size and density of the device? An extra high density screen (xhdpi) device like Galaxy Nexus will need a larger cache to hold the same number of images in memory compared to a device likeNexus S (hdpi).
  • What dimensions and configuration are the bitmaps and therefore how much memory will each take up?
  • How frequently will the images be accessed? Will some be accessed more frequently than others? If so, perhaps you may want to keep certain items always in memory or even have multiple LruCache objects for different groups of bitmaps.
  • Can you balance quality against quantity? Sometimes it can be more useful to store a larger number of lower quality bitmaps, potentially loading a higher quality version in another background task.

다음은 LruCache로 구현한 샘플임


private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
   
...
   
// Get max available VM memory, exceeding this amount will throw an
   
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
   
// int in its constructor.
   
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

   
// Use 1/8th of the available memory for this memory cache.
   
final int cacheSize = maxMemory / 8;

    mMemoryCache
= new LruCache<String, Bitmap>(cacheSize) {
       
@Override
       
protected int sizeOf(String key, Bitmap bitmap) {
           
// The cache size will be measured in kilobytes rather than
           
// number of items.
           
return bitmap.getByteCount() / 1024;
       
}
   
};
   
...
}

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
   
if (getBitmapFromMemCache(key) == null) {
        mMemoryCache
.put(key, bitmap);
   
}
}

public Bitmap getBitmapFromMemCache(String key) {
   
return mMemoryCache.get(key);
}


비트맵을 로드하기전에 캐시에 비트맵이 있는지 확인하고 로드한다.

public void loadBitmap(int resId, ImageView imageView) {
   
final String imageKey = String.valueOf(resId);

   
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
   
if (bitmap != null) {
        mImageView
.setImageBitmap(bitmap);
   
} else {
        mImageView
.setImageResource(R.drawable.image_placeholder);
       
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task
.execute(resId);
   
}
}


BitmapWorkerTask는 로드가 완료되면 메모리캐시에 추가한다.


class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
   
...
   
// Decode image in background.
   
@Override
   
protected Bitmap doInBackground(Integer... params) {
       
final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources
(), params[0], 100, 100));
        addBitmapToMemoryCache
(String.valueOf(params[0]), bitmap);
       
return bitmap;
   
}
   
...
}


디스크 캐시 이용하기

메모리캐시만으로는 부족하다. 너무 많이 사용하는것 이외에도 전화가 온다던가 하여 앱이 백그라운드상태가 되었을 때 앱은 종료가 될 수 있고 그럴경우 캐시도 제거될것이다. 앱이 다시 로드되면 또다시 이미지를 로드해야 할것이다. 이를 개선하기 위해 디스크에 캐시를 하고 디스크에 캐시가 있으면 디스크에서 읽어들이는 방법이 있다. 메모리캐시보다는 느리지만 네트워크보다는 빠르며 메모리캐시와 함께 사용해도 될것이다.

** 갤러리앱에서 관리하는 이미지들을 읽어들일때는 ContentProvider를 이용하면 더 빠르게 접근이 가능하다.

안드로이드 소스에서 사용중인 DiskLruCache를 사용하는 샘플코드이다.


private DiskLruCache mDiskLruCache;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";

@Override
protected void onCreate(Bundle savedInstanceState) {
   
...
   
// Initialize memory cache
   
...
   
// Initialize disk cache on background thread
   
File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
   
new InitDiskCacheTask().execute(cacheDir);
   
...
}

class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
   
@Override
   
protected Void doInBackground(File... params) {
       
synchronized (mDiskCacheLock) {
           
File cacheDir = params[0];
            mDiskLruCache
= DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
            mDiskCacheStarting
= false; // Finished initialization
            mDiskCacheLock
.notifyAll(); // Wake any waiting threads
       
}
       
return null;
   
}
}

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
   
...
   
// Decode image in background.
   
@Override
   
protected Bitmap doInBackground(Integer... params) {
       
final String imageKey = String.valueOf(params[0]);

       
// Check disk cache in background thread
       
Bitmap bitmap = getBitmapFromDiskCache(imageKey);

       
if (bitmap == null) { // Not found in disk cache
           
// Process as normal
           
final Bitmap bitmap = decodeSampledBitmapFromResource(
                    getResources
(), params[0], 100, 100));
       
}

       
// Add final bitmap to caches
        addBitmapToCache
(imageKey, bitmap);

       
return bitmap;
   
}
   
...
}

public void addBitmapToCache(String key, Bitmap bitmap) {
   
// Add to memory cache as before
   
if (getBitmapFromMemCache(key) == null) {
        mMemoryCache
.put(key, bitmap);
   
}

   
// Also add to disk cache
   
synchronized (mDiskCacheLock) {
       
if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
            mDiskLruCache
.put(key, bitmap);
       
}
   
}
}

public Bitmap getBitmapFromDiskCache(String key) {
   
synchronized (mDiskCacheLock) {
       
// Wait while disk cache is started from background thread
       
while (mDiskCacheStarting) {
           
try {
                mDiskCacheLock
.wait();
           
} catch (InterruptedException e) {}
       
}
       
if (mDiskLruCache != null) {
           
return mDiskLruCache.get(key);
       
}
   
}
   
return null;
}

// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
public static File getDiskCacheDir(Context context, String uniqueName) {
   
// Check if media is mounted or storage is built-in, if so, try and use external cache dir
   
// otherwise use internal cache dir
   
final String cachePath =
           
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
                   
!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
                            context
.getCacheDir().getPath();

   
return new File(cachePath + File.separator + uniqueName);
}


** 위 예제에서 Lock 객체는 디스크 작업중에 캐시에 접근하는것을 막아준다.

메모리캐시가 UI thread에서 확인되는 반면 디스크캐시는 백그라운드에서 확인한다. 디스크작업은 절대 UI thread에서 구동되면 안된다. 이미지프로세싱이 끝나면 비트맵은 메모리와 디스크캐시 2군데 추가된다.


Configuration change에 대한 처리


런타입에 설정이 변경되게 되면 가령 스크린오리엔테이션 안드로이드는 액티비티를 제거하였다가 재시작한다. 이 때 이미지처리를 간결하게 하여 사용성을 해쳐서는 안될것이다.


Fragment를 사용하는 경우 Fragment에 setRetainInstance(true)를 호출함으로써 메모리캐시의 참조를 통해 캐시를 유지하고 참조할 수 있다.


private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
   
...
   
RetainFragment retainFragment =
           
RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    mMemoryCache
= retainFragment.mRetainedCache;
   
if (mMemoryCache == null) {
        mMemoryCache
= new LruCache<String, Bitmap>(cacheSize) {
           
... // Initialize cache here as usual
       
}
        retainFragment
.mRetainedCache = mMemoryCache;
   
}
   
...
}

class RetainFragment extends Fragment {
   
private static final String TAG = "RetainFragment";
   
public LruCache<String, Bitmap> mRetainedCache;

   
public RetainFragment() {}

   
public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
       
RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
       
if (fragment == null) {
            fragment
= new RetainFragment();
            fm
.beginTransaction().add(fragment, TAG).commit();
       
}
       
return fragment;
   
}

   
@Override
   
public void onCreate(Bundle savedInstanceState) {
       
super.onCreate(savedInstanceState);
       
setRetainInstance(true);
   
}
}





Posted by 삼스
iOS2015. 4. 3. 15:16



objective C에 비해 swift의 생소한것들 중에 optional value란것이 있다.

자바의 Nullable과 Nonnull과는 좀 마이 다르다.

어떤 변수(값이나 참조모두)에 대해 nil을 허용할것인가에 대한 옵션이 되겠는데 이게 무슨소용인가 싶을 것이다.

기존에 코딩시 습관적으로 if (someobject != nil) 을 삽입하던 코드를 좀더 간결하고 버그가능성을 줄일 수 있도록 많은 고민을 한듯 하다.


먼저 변수를 선언하는 방법은 그냥 선언하는것과 Optional로 선언하는 방법이 있겠다.

var normalValue: String = "samse"

var optionalValue: String?


optional의 의미는 nil을 허용하는가에 대한 것이다 따라서 아래 코드는 에러다.


normalValue = nil // Type 'String' does not conform to protocol 'NilLiteralConvertible'


아래코드는 정상코드이다.


optionalValue = nil


optional 변수는 Explicit optional 변수와 Implicit optional 변수가 있다.

?를 붙인것이 Explicit이고 !를 붙인것이 implicit이다.


var explicitValue : String?

var implicitValue : String!


Explicit변수는 참조 시 nil인지 확인을 하기 위해 Forced unwrapping을 해야 한다. 해당 변수에 !를 붙임으로서 forced unwrapping이 수행된다

.

var unwrappedExplicitValue = optionalValue!


반드시 forced unwrapper를 통해서 값을 추출해서 사용해야 에러가 발생하지 않는다.


if unwrapperExplicitValue != nil {

    println( unwrapperExplicitValue)

}

Implicit 변수는 참조시 unwrapper를 안해도 되나 런타임에 오류가 발생 가능하므로 항상 주의해야 한다.


if implicitValue != nil {

    println( implicitValue )

}

Optional binding을 통해서 코드를 좀더 간결하게 할 수 있다.


if let value = explicitValue { // explicitValue! 로 표기하지 않는다.

  println("Value = \(value)")

}


형식은 아래와 같다.


if let [새변수명] = [optional 변수 또는 optional변수를 반환하는 함수] { // 

  ..

}


위에서 [새변수명]은 scope이 if문 내 이다.

if문내에서 [새변수명]을 변경하고자 할 경우에는 let이 아니라 var를 사용한다.


if var value = explicitValue {   

    value = "changed to SAMSE"

    println("Value = \(value)")

}


값이 nil인 경우 디폴트값을 사용하고자 할 수 있는데 이 때 ?? 연산자를 제공한다.


var value = explicitValue ?? "SAMSE"

위 코든는 explicitValue가 평가한바 nil이면 value에 "SAMSE"를 대입한다.



클래스도 참조로서 당연히 optional로 선언가능하고 사용가능한데 class내의 optional 변수를 참조하는데 좀더 간결하게 접근하는 방법을 제공한다. 애플개발자들이 많은 고민을 하면서 어떻게든 코드양을 줄이고 편하게 코딩하고자 한 노력이 보이는 부분이다.

class Person {

    var contact: Contact?

    init() {}

}

class Contact {

    var address: String?

    var tel: String?

    var email: String?

    var extra: String = "admin@gmail.com"

    init() {}

}


위 와 같이 클래스를 정의하고 아래처럼 Persion객체를 생성하였다.


var p = Person()


클래스 선언부에서 변수선언시 초기화하지 않았기 때문에 초기값은 extra를 제외하고 optional인 변수들은 모두 nil이 된다.

따라서 아래 코드는 p의 contact가 nil이기 때문에 컴파일 에러가 발생한다.


var email = p.contact!.email


nil 체크를 반드시 해주어야 한다.


if let contact = p.contact {

    if let email = contact.email! {

        println(email)

    }

    else {

        println("nil email")

    }

}

else {

    priintln("nil contact")


이 때 위 코드를 간결하게 줄여주는 방법이 Optional Chaining 표기법이다. 이는 변수 뒤에 ?를 붙임으로서 가능하다. 

다음 코드는 정상이다.


var email2 = p.contact?.email?


이 코드는 p.contact가 nil이므로 검사를 통해 email2에 nil을 대입한다. optional binding과 함께하여 다음과 같이 코드 작성이 가능하다.


if let email = p.contact?.email? {

    println('email = \(email)")

} else {

    println("email is nil")

}


email is nil이 표시될것이다.


값을 대입할 때도 chaining을 통해 nil이 반환되면 해당 변수에 값이 대입되지 않는다.


p.contact?.email? = "someone@gmail.com"

if let email = p.contact?.email? {

    println("Email : \(email)")

} else {

    println("Email is nil yet")

}


마찬가지로 Email is nil yet이 표시될것이다.


아래와 같이 contact를 유효하게 만들어주면 대입이 될것이다.


p.contact = Contact()

p.contact?.email? = "someone@gmail.com"


if let email = p.contact?.email? {

    println("Email : \(email)")

else {

    println("Email is nil yet")

}

Email : someone@gmail.com 이 표시될것이다.


Contact클래스의 extra field는 optional 변수가 아니다.


class Contact {

    var address: String?

    var tel: String?

    var email: String?

    var extra: String = "admin@gmail.com"

    init() {}

}


이는 optional chaing으로 접근 시 타입은 String형식이더라도 옵셔널인 String?으로 받게됨을 알아야 한다. <-- 중요 중요!!!

let extra = p.contact?.extra! // p.contact?extra String? 반환함으로써 타입캐스팅 에러 발생


이는 다음과 같이 수정되어야 한다.


let extra = (p.contact?.extra)!

또는

if let extra = p.contact?.extra {

    // ...

}

애플개발자가 고민하여 만든것 같은데 nil에 대한 대비를 코딩레벨로 끌어내려서 런타임에러가 많이 줄어들것으로 예상된다. 실제 개발하면서 빠르게 익숙해져야 할듯 하다.





Posted by 삼스