IT_Programming/Android_Java

[펌] AsyncQueryHandler를 이용한 콘텐트 프로바이더(ContentProvider) 접근

JJun ™ 2016. 8. 31. 12:48



 출처: http://androidyongyong.tistory.com/18





이번시간은 AsyncQueryHandler 를 사용하여 콘텐트 프로바이더에서 비동기적 CRUD 작업 처리를 전문으로 하는 유틸리티 클래스이다.
클래스는 UI스레드에서 콘텐트 프로바이드 작업을 떠넘기는 데 사용되고, UI 스레드는 백그라운드 태스크가 완료되면 수신한다.


다음 주제에 대해 배워보도록하자

  • 콘텐트 프로바이더의 기본과 동시 접근
  • AsyncQueryHandler를 구현하고 사용하는 방법
  • 백그라운드 실행에 대한 이해해


13.1 콘텐트 프로바이더에 대한 간략한 소개

콘텐트 프로바이더는 데이터베이스 중심의 네 가지 접근 메서드인 CRUD 접근 방식을 통해 데이터를 읽고, 추가하고, 변경하고,
삭제할 수 있는 인터페이스를 제공한다.


커스텀 프로바이더
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
public class EatContentProvider extends ContentProvider {
    private final static String STRING_URI = "content://com.eat.provider/resource";
    public final static Uri CONTENT_URI = Uri.parse(STRING_URI);
 
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selections, String sortOrder) {
         // 데이터 소스 읽기
         return null;
     }
 
     @Override
    public Uri insert(Uri uri, ContentValues values) {
        //데이터 추가
        return null;
    }
 
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        //데이터 제거
        return 0;
    }
 
    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        //데이터 변경
        return 0;
    }
 
}

SQLite 데이터베이스는 응용 프로그램 전용이지만, ContentProvider 클래스를 통해 다른 응용프로그램에도 노출할 수 있다.
콘텐트 프로바이더의 정의는 AndroidManifest.xml 에서 수행된다. 여기서 다른 응용프로그램에서 접근하는 여부는 exported 속성으로 결정한다.


 <provider
     android:name="EatContentProvider"
     android:authorities="com.eat.provider"
     android:exported="true" />



ContentResolver는 프로바이더와 동일한 범위의 데이터 접근 메서드(query, insert, delete, update)를 포함한다.
해당 메서드를 호출하기 위해선 Uri의 리졸버로 query 메서드를 호출하면 된다.


  public final static Uri CONTENT_URI = Uri.parse("content://com.eat.provider/resource");

  ContentResolver cr = getContentResolver();
  Cursor c = cr.query(CONTENT_URI, null, null, null, null);




13.2 콘텐트 프로바이더의 백그라운드 처리에 대한 정당성

콘텐트 프로바이더는 데이터에 접근하는 클라이언트의 개수 또는 얼마나 많은 클라이언트를 동시에 처리할 수 있는지 제어할 수 없다.
스레드로부터 안전하지 않으면 여러 프로바이더에 의해서 동시 접근이 데이터 불일치로 이어질 수 있다.

그러나 SQLite 데이터 베이스 접근은 데이터베이스의 트렌잭션 모델이 순차적이기 때문에 그 자체로 스레드에 안전하다.
즉, 데이터 동시 접근으로 손상될 수 없다.


미리 쓰기 로깅을 이용해서 뼝렬로 데이터 베이스 접근이 가능하다.


 SQLiteDatabase db = SQLiteDatabase.openDatabase(...);

 db.enableWriteAheadLogging();


즉, 쓰기는 활성화된 읽기 트랜젝션이 있을 때 원본 데이터베이스에 기록되지 않고 데이터베이스의 복사본에 수행된다.
콘텐트 프로바이더에 대한 접근은 일반적으로 저장소와의 상호작용을 한다. 

이 작업은 긴 태스크이기 때문에 UI스레드에서 실행하면 안되고, 백그라운드 스레드가 실행시켜야 한다.

ContentResolver를 호출하는 응용프로그램처럼 프로바이더의 사용자에 의해 백그라운드 스레드에서 생성해야 한다.
다른 프로세스에서 호출 시 바인더 스레드에서 호출한다.

콘텐트 프로바이더에 저장된 데이터는 대부분 UI 스레드에서 처리된다.
그러나 프로바이더는 UI스레드에 직접 접근할 수 없기 때문에 비동기 메커니즘을 이용한다.
안드로이드 플랫폼에서는 프로바이더를 위한 두 가지 특별한 매커니즘이 있는데 AsyncQueryHandler와 CursorLoader가 있다. 


13.3 AsyncQueryHandler 사용

AsyncQueryHandler는 ContentResolver, 백그라운드 실행, 스레드간 메시지 전달을 처리함으로써
콘텐트 프로바이더에 대한 비동기 접근을 간소해 주는 추상 클래스이다.


AsyncQueryHandler의 네 가지 메서드

  • final void startDelete(int token, Object cookie, Uri uri, String selection, String[] selectionArgs)
  • final void startInsert(int token, Object cookie, Uri uri, ContentValues initialValues)
  • final void startQuery(int token, Object cookie, Uri uri, String[] projection, String selection, String orderBy)
  • final void startUpdate(int token, Object cookie, Uri uri, ContentValues values, String selection, String[] selectionArgs)
프로바이더 동작이 완료되면, AsyncQueryHandler로 결과를 반환한다.

Token : 요청 유형, 요청이 생성될 수 있다면 그 유형을 정의한다. 미처리 요청이 취소될 수 있도록 요청을 식별하기도 한다.

Cookie : 요청 식별자 및 임의 객체 유형의 데이터 컨테이너

예제를 통해 사용을 알아보자
예제 : 연락처 확장 리스트
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
public class ExpandableContentListActivity extends ExpandableListActivity {
    private static final String[] CONTACT_PROJECTION = new String[] {
            ContactsContract.Contacts._ID,
            ContactsContract.Contacts.DISPLAY_NAME
    };
    private static final int GROUP_ID_COLUMN_INDEX = 0;
    private static final String[] PHONE_NUMBER_PROJECTION = new String[] {
            ContactsContract.CommonDataKinds.Phone._ID,
            ContactsContract.CommonDataKinds.Phone.NUMBER
    };
 
    private static final int TOKEN_GROUP = 0;
    private static final int TOKEN_CHILD = 1;
 
    private QueryHandler mQueryHandler;
    private CursorTreeAdapter mAdapter;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        //어뎁터 설정
        mAdapter = new MyExpandableListAdapter(this, android.R.layout.simple_expandable_list_item_1,
                android.R.layout.simple_expandable_list_item_1,
                new String[] {ContactsContract.Contacts.DISPLAY_NAME},
                new int[] {android.R.id.text1},
                new String[] {ContactsContract.CommonDataKinds.Phone.NUMBER},
                new int[] {android.R.id.text1});
 
        setListAdapter(mAdapter);
 
        mQueryHandler = new QueryHandler(this, mAdapter);
 
        //사람을 찾기 위한 쿼리
        mQueryHandler.startQuery(TOKEN_GROUP,
                null,
                ContactsContract.Contacts.CONTENT_URI,
                CONTACT_PROJECTION,
                ContactsContract.Contacts.HAS_PHONE_NUMBER,
                null,
                ContactsContract.Contacts.DISPLAY_NAME + "ASC");
    }
 
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mQueryHandler.cancelOperation(TOKEN_GROUP);
        mQueryHandler.cancelOperation(TOKEN_CHILD);
    }
 
    private static final class QueryHandler extends AsyncQueryHandler {
        private CursorTreeAdapter mAdapter;
 
        public QueryHandler(Context context, CursorTreeAdapter adapter) {
            super(context.getContentResolver());
            this.mAdapter = adapter;
        }
 
        @Override
        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
            switch (token) {
                case TOKEN_GROUP:
                    mAdapter.setGroupCursor(cursor);
                    break;
                case TOKEN_CHILD:
                    int groupPosition = (Integer) cookie;
                    mAdapter.setChildrenCursor(groupPosition, cursor);
                    break;
            }
        }
    }
 
 
    public class MyExpandableListAdapter extends SimpleCursorTreeAdapter {
 
        // 생성자가 커서를 인수로 받지 않음을 주목하라
        // 이는 메인 스레드에서 데이터베이스 쿼리를 피하기 위함이다.
        public MyExpandableListAdapter(Context context, int groupLayout, int childLayout, String[] groupFrom, int[] groupTo, String[] childrenFrom, int[] childrenTo) {
            super(context, null,groupLayout, groupFrom, groupTo, childLayout, childrenFrom, childrenTo);
        }
 
 
        @Override
        protected Cursor getChildrenCursor(Cursor cursor) {
            // 주어진 그룹에서 모든 하위 항목에 대한 커서를 반환한다.
            // 연락처의 전화번호를 가르키는 커서를 반환한다.
            Uri.Builder builder = ContactsContract.Contacts.CONTENT_URI.buildUpon();
            ContentUris.appendId(builder, cursor.getLong(GROUP_ID_COLUMN_INDEX));
            builder.appendEncodedPath(ContactsContract.Contacts.Data.CONTENT_DIRECTORY);
            Uri phoneNumbersUri = builder.build();
 
            mQueryHandler.startQuery(TOKEN_CHILD,
                    cursor.getPosition(),
                    phoneNumbersUri,
                    PHONE_NUMBER_PROJECTION,
                    ContactsContract.CommonDataKinds.Phone.MIMETYPE + "=?",
                    new String[] {ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE },
                    null);
            return null;
        }
    }
}


13.3.2 AsyncQueryHandler 이해

AsyncQueryHandler는 ContentResolver 를 보유하고 백그라운드 스레드 안팍으로 스레드 통신을 처리한다.
프로바이더 요청 중 하나가 호출 될때 백그라운드의 메시지 큐에 추가된다.


13.3.3 한계

AsyncQueryHandler의 장점은 간소함이다. 

그러나 다음과 같은 한계가 있다.

  • 일괄작업
    일괄작업 같은 경우는 이를 지원하기 위해 ContentProviderOperation 클래스를 API 레밸 5에 추가하였다. 

  • CancellationSignal
    API 레벨 16은 CancellationSignal 을 추가했다. 이를 사용하면, 쿼리를 취소할 수 있지만, AsyncQueryHandler를 지원하지 않으므로
    여전히 cancelOperation(true) 를 사용해야 한다.


13.4 마치며

AsyncQueryHandler는 콘텐트 프로바이더에 대한 전체 CRUD 작업 세트에 접근할 떄 사용하기 쉬운 비동기 메커니즘으로 구성된다.
그러나 이것은 안드로이드 플랫폼 최근 버전에서 추가된 몇가지 최신 기능은 지원하지 않는다.
따라서 다음 장에서 설명 할 CursorLoader와 조합해서 사용하는 것이 좋다.
즉, 데이터 쿼리는 CursorLoader에 의해 처리하고, 삽입/업데이트/삭제는 AsyncQueryHandler에 의해 처리하면 좋다.