본문 바로가기

카테고리 없음

Android 이미지 처리 메모리 관리 팁.

출처 : http://www.yeory.com/138

 
Android에서 Bitmap 관련 작업을 할때 항상 사용하는것이 Bitmap 클래스와 BitmapFactory 클래스이다.

BitmapFactory 클래스는 decode 메서드를 사용하여 File, Stream 등을 입력받아 Bitmap으로 변환할 수 있다.
Bitmap 클래스는 Bitmap의 재가공, Bitmap의 구성을 변경한다던지, 크기를 변경하는 작업을 수행한다.

그런데 현재 Android 상에서 위 2개는 심각한 메모리 누수를 유발하고 있다.

단순 SD 카드에서 파일을 읽어 와 표시해주는 것이라면 관계가 없지만 

작업 1. MediaStore를 사용하여 이미지를 가지고 온 후 크기를 변경하고 이를 화면에 표시함과 동시에 서버에 업로드 한다.

위 작업이 한번이 아닌 여러번 수행 되어야 한다면..? 3번? 많게는 5번 이내로 오류나서 어플이 종료가 된다.;;

오류를 발생한 작업 

?
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
Bitmap resized = null;
Bitmap orgBitmap = null;
File targetFile = null;
int[] size = null;
 
// 이미지 품질 옵션
options.inTempStorage = new byte[16*1024];
options.inSampleSize = 1;
 
switch(requestCode){
    case CommonResource.SHOW_ALBUM:
        photoUri = data.getData();
         
        // 갤러리에서 선택했을때 실제 경로 가져오기 위해 커서
        String [] proj={MediaStore.Images.Media.DATA}; 
        Cursor cursor = managedQuery( photoUri, 
                proj, // Which columns to return 
                null,       // WHERE clause; which rows to return (all rows) 
                null,       // WHERE clause selection arguments (none) 
                null); // Order-by clause (ascending by name) 
        int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); 
        cursor.moveToFirst(); 
       
        photoUri = Uri.parse(cursor.getString(column_index));
        // 비트맵을 먼저 만들고
        orgBitmap = BitmapFactory.decodeFile(photoUri.getPath(), options);
        // 원본 사이즈에 따른 새로운 사이즈 계산
        size = commonUtil.getBitmapSize(orgBitmap);
        // 비트맵 생성
        displayBitmap = Bitmap.createScaledBitmap(orgBitmap, size[0], size[1], true);
         
        // 디바이스에 맞게 비트맵 생성 후 업로드 하여야 하므로 임시 파일을 생성해준다.
        // MediaStore에는 원본이, 아래는 복사본을 생성하여 업로드 하는 것.
        try {
            File targetFolder = new File(targetPath);
            if(!targetFolder.exists()){
                targetFolder.mkdirs();
                targetFolder.createNewFile();
            }
            if(targetFile == null)
                targetFile = new File(targetFolder, UUID.randomUUID().toString());
             
            FileOutputStream out = new FileOutputStream(targetFile);
            displayBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
            out.flush();
            out.close();
             
            uploadImagePath = targetFile.getAbsolutePath();
             
        } catch (Exception e) {
               e.printStackTrace();
        } finally{
            targetFile = null;
            uploadImage.setImageBitmap(commonUtil.getRoundedCornerBitmap(this, displayBitmap, 10));
        }
        break;
    }

위 소스에서 orgBitmap에 앨범에서 사진을 하나 불러온다. 이 사진은 사이즈가 허벌나다. (orgBitmap)
그리고 이 사진을 그대로 업로드 할 시 트래픽도 크고 시간도 오래 걸릴게 뻔하다 생각하여 
비율에 맞게 다시 크기를 조정하여 비트맵을 생성하게 했다. (displayBitmap)
그리고 업로드를 위한 임시파일을 생성하도록 코드가 이어지고
마지막으로 화면에 표시할때는 round 처리까지 한 후 보여지게 되어 있다.

그지같게도 비트맵을 생성한 후 메모리가 반환이 되지 않는다. 
절대... 쓸일 없는 Bitmap은 recycle()하면 된다지만 역시 일부다.

위 작업을 두번하니까 어플이 그냥 죽는다. 카메라로 촬영을 하든 앨범에서 가지고 오든 죽는다.

원인은 비트맵의 반복 생성. 
createScale을 사용하던 decode를 하던 한번 만드는 것도 메모리를 상당부분 사용하는데 이를 중복으로 사용하는게 문제라는 것.

그래서 아래와 같이 코드를 바꾸기로 했다.

?
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
26
27
28
// 이미지 품질 옵션
options.inJustDecodeBounds = true;
 
switch(requestCode){
    case CommonResource.SHOW_ALBUM:
        photoUri = data.getData();
             
        // 갤러리에서 선택했을때 실제 경로 가져오기 위해 커서
        String [] proj={MediaStore.Images.Media.DATA}; 
        Cursor cursor = managedQuery( photoUri, 
                proj, // Which columns to return 
                null,       // WHERE clause; which rows to return (all rows) 
                null,       // WHERE clause selection arguments (none) 
                null); // Order-by clause (ascending by name) 
        int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); 
        cursor.moveToFirst(); 
       
        photoUri = Uri.parse(cursor.getString(column_index));
        uploadImagePath = photoUri.getEncodedPath();
        displayBitmap = BitmapFactory.decodeFile(uploadImagePath, options);
         
        options = commonUtil.getBitmapSize(options);
        displayBitmap = BitmapFactory.decodeFile(uploadImagePath, options);
         
        uploadImage.setImageBitmap(displayBitmap);
         
        break;
}

?
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
    public Options getBitmapSize(Options options){
        int targetWidth = 0;
        int targetHeight = 0;
         
        if(options.outWidth > options.outHeight){   
            targetWidth = (int)(600 * 1.3);
            targetHeight = 600;
        }else{
            targetWidth = 600;
            targetHeight = (int)(600 * 1.3);
        }
 
        Boolean scaleByHeight = Math.abs(options.outHeight - targetHeight) >= Math.abs(options.outWidth - targetWidth);
        if(options.outHeight * options.outWidth * 2 >= 16384){
            double sampleSize = scaleByHeight
                ? options.outHeight / targetHeight
                : options.outWidth / targetWidth;
            options.inSampleSize = (int) Math.pow(2d, Math.floor(Math.log(sampleSize)/Math.log(2d)));
        }
        options.inJustDecodeBounds = false;
        options.inTempStorage = new byte[16*1024];
         
        return options;
    }
Bitmap으로 생성하진 않고 먼저 decoding만 한 후 크기를 가져와 실제 맞추려는 크기와 비교하여 size 조절이 필요하면
사이즈를 조절하도록 option을 설정하면 된다.
구글링 하다 보니 누가 만들어 놓았더라. (출처는 소스에..)

아.. 맘 놓고 해결..