[android] 오프라인 Cursor

사건의 발단은 만들고 있는 앱이 멀쩡하다가 onResume타이밍에 죽어버리는 현상이 생기는 것부터였습니다.

android.database.StaleDataException: Attempted to access a cursor after it has been closed.

이런 메세지로 죽는건데 앱내에서 SQLite를 쓰다보니 뭔가 커서를 꼼꼼하게 close안해줘서 그런가 보다 하고 열심히 cursor.close()를 하고 다녔습니다.

그런데도 전혀 문제가 해결되지 않고 있었습니다.

그래서 이전 포스트에 소개한 커서기반의 어뎁터가 문제인가 싶었습니다. 그넘이 커서에 의존하니까 뭔가 디비연결이 끊어지고 나서 다시 복귀할때 어뎁터가 문제를 일으켜서 죽는건가 싶은거였죠.

이 문제를 해결하기 위해 더이상 연결을 유지하는 Cursor를 버리고 사본을 떠서 오프라인 상태에서 쓸 수 있는 커서를 만들기로 했습니다.

커서 인터페이스

안드로이드의 커서 문서를 참조하면 다음과 같은 정도의 인터페이스가 추출됩니다.

public void close()
public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer)
public void deactivate()
public byte[] getBlob(int columnIndex)
public int getColumnCount()
public int getColumnIndex(String columnName)
.
.
.

(머가 이리 많냐.^^;)

이 중 오프라인커서의 특징을 고려해서 다음 메서드는 그냥 방치하고 유지만 하기로 결정했습니다. 짜피 Cursor 인터페이스를 구상할 것이므로 전부 만들긴 해야합니다. 다른건 그렇다치고

getColumnIndexOrThrow

는 귀찮아서 방치해버렸습니다 ==;

//방치된 메서드 ^^;
public boolean isClosed(){return true;}
public int getColumnIndexOrThrow( String s ) throws IllegalArgumentException{return 0;}
public boolean getWantsAllOnMoveCalls(){return false;}
public void close(){}
public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer){}
public void deactivate(){}
public Uri getNotificationUri(){return null;}
public Bundle getExtras(){return null;}
public void registerContentObserver(ContentObserver observer){}
public void registerDataSetObserver(DataSetObserver observer){}
public boolean requery(){return false;}
public Bundle respond(Bundle extras){return null;}
public void setNotificationUri(ContentResolver cr, Uri uri){}
public void unregisterContentObserver(ContentObserver observer){}
public void unregisterDataSetObserver(DataSetObserver observer){}

이제 남은건 데이터와 커서 관련된 메서드들 뿐입니다.

 
 
 

복사본을 위한 필드 구성

커서한판을 완전히 복사해야하므로 거기에 합당한 자료구조를 생성했습니다.

//레코드셋
private ArrayList<ArrayList<Object>> _rs = new ArrayList<ArrayList<Object>>();

//현재 레코드
private ArrayList<Object> _row;

//필드명
private ArrayList<String> _fields = new ArrayList<String>();

//필드명의 배열
private String[] _fieldsArray;

//필드의 데이터타입
private ArrayList<Integer> _fieldTypes = new ArrayList<Integer>();

//레코드셋길이, 컬럼길이, 현재 커서 위치
private int _length, _columeCount, _cursor;

필드명의 배열은

String[] getColumnNames()

에 대응하기 위해서 만들었습니다. 이 정도로 필드를 정의하고 생성자에서 Cursor 의 복사본을 만듭니다.

Cursor의 복사본 생성

결국 핵심은 Cursor의 복사본을 뜨는 것입니다. 생성자는 Cursor를 받아 열심히 복사본을 만듭니다.

public bsCursor( Cursor c ){

	//커서가 있다면
	if( c == null ) return;

	_length = c.getCount(); //길이설정

	//처음으로 이동
	if( _length == 0 || !c.moveToFirst() ) return;

	//컬럼길이 얻고
	int i, j = _columeCount = c.getColumnCount();

	//우선 필드이름과 필드 타입을 정리함
	for( i = 0; i < j ; i++ ){
		_fields.add( c.getColumnName( i ) );
		_fieldTypes.add( c.getType( i ) );
	}

	//필드이름배열 생성
	_fieldsArray = (String[])_fields.toArray(new String[_fields.size()]);

	//본격적인 레코드복사
	do{

		//하나의 row를 생성하고
		ArrayList<Object> _row = new ArrayList<Object>();

		//필드수만큼 루프를 돌며 각타입에 맞게 넣어준다.
		for( i = 0; i < j ; i++ ){
			switch(_fieldTypes.get(i)){
			case Cursor.FIELD_TYPE_NULL: _row.add(null); break;
			case Cursor.FIELD_TYPE_INTEGER: _row.add(c.getInt(i)); break;
			case Cursor.FIELD_TYPE_FLOAT: _row.add(c.getFloat( i )); break;
			case Cursor.FIELD_TYPE_STRING: _row.add(c.getString( i )); break;
			case Cursor.FIELD_TYPE_BLOB: _row.add(c.getBlob( i )); break;
			}
		}

		//레코드셋에 추가
		_rs.add( _row );

	}while( c.moveToNext() );

	//커서와 현재 레코드를 초기화함
	_cursor = 0;
	_row = _rs.get( 0 );
}

모든 프로그래밍이 그렇듯 노가다 이상도 이하도 존재하지 않습니다 ^^; 이렇게 일단 복사본을 만들었으니 각 메서드를 구상할 차례입니다.

필드의 데이터를 얻는 메서드

getXXX 시리즈로 타입을 명시해 얻는 메서드는 매우 간단히 구현됩니다.

//머..걍 형변환하면 됨
public byte[] getBlob(int columnIndex){return (byte[])_row.get(columnIndex);}

public double getDouble(int columnIndex){return (Double)_row.get(columnIndex);}

public float getFloat(int columnIndex){return (Float)_row.get(columnIndex);}

public int getInt(int columnIndex){return (Integer)_row.get(columnIndex);}

public long getLong(int columnIndex){return (Long)_row.get(columnIndex);}

public short getShort(int columnIndex){return (Short)_row.get(columnIndex);}

public String getString(int columnIndex){return (String)_row.get(columnIndex);}

자세한 설명은 생략..

커서 및 메타데이터를 얻는 계열

이미 내부 필드에 필요한 모든 구조체가 생성되어있으므로 마찬가지로 손쉽게 구상됩니다.

public int getColumnCount(){return _columeCount;}
public int getColumnIndex(String columnName){return _fields.indexOf(columnName);}
public String getColumnName(int columnIndex){return _fieldsArray[columnIndex];}
public String[] getColumnNames(){return _fieldsArray;}
public int getCount(){return _length;}
public int getPosition(){return _cursor;}
public int getType(int columnIndex){return _fieldTypes.get( columnIndex );}

너무 간단하여 딱히 뭘 설명할것도 없네요 ^^ 그냥 있는거 맞춰서 공개하면 됩니다.

커서컨트롤 계열

이제 마지막으로 커서를 컨트롤하는 부분만 남았습니다.

가장 복잡하다고는 하나 이미 _cursor, _length, _row 기반이 확고하므로 그저 산수만 남아있습니다.

public boolean isFirst(){return _cursor == 0;}
public boolean isLast(){return _cursor == _length - 1;}
public boolean isNull(int columnIndex){return _row.get( columnIndex ) == null;}

public boolean move(int offset){
	int i = _cursor + offset;
	if( i < _length ){
		_cursor = i;
		_row = _rs.get(_cursor);
		return true;
	}else return false;
}
public boolean moveToFirst(){
	_cursor = 0;
	_row = _rs.get(_cursor);
	return true;
}
public boolean moveToLast(){
	_cursor = _length - 1;
	_row = _rs.get(_cursor);
	return true;
}
public boolean moveToNext(){
	if( _cursor < _length - 1 ){
		_cursor++;
		_row = _rs.get(_cursor);
		return true;
	}else return false;
}
public boolean moveToPosition(int position){
	if( position > -1 && position < _length ){
		_cursor = position;
		_row = _rs.get(_cursor);
		return true;
	}else return false;
}
public boolean moveToPrevious(){
	if( _cursor > 0 ){
		_cursor--;
		_row = _rs.get(_cursor);
		return true;
	}else return false;
}

요정도로 전부를 구상하고 나면 오프라인 커서가 탄생하여 다음과 같이 사용할 수 있습니다.

//디비용 커서를 얻어서
Cursor c = select();

//bsCursor로 둔갑시키면 오프라인 커서가 됨
Cursor cursor = new bsCursor( c );

//원본은 닫아버리고
c.close();

//이제 bsCursor를 사용하면 됨
cursor.moveToFirst();
Log.i( cursor.getString(0) );

결론

오늘의 결론은 엄청나게 황당한데, 결국 버그의 원인은 이게 아니었습니다. 버그의 원인은 다음과 같은 링톤매니져로부터 얻은 커서가 원인입니다.

Cursor c = ringtoneMgr.getCursor();

링톤매니져로부터 얻은 커서는 close하면 에러가 납니다. 그냥 열어둬야합니다. 끝 =.=;