면접 질문 목록으로 가기
면접 질문실전 질문꼬리 질문

ContentProvider 시작 순서와 ContentResolver 뒤의 CursorWindow

skydovesJaewoong Eum (skydoves)||11분 소요

ContentProvider 시작 순서와 ContentResolver 뒤의 CursorWindow

ContentProvider는 보통 앱 간에 데이터를 공유하는 수단으로 설명되지만, 수많은 라이브러리의 동작을 설명해 주는 두 번째 역할이 있습니다. 시스템은 Application.onCreate()를 호출하기 전에, 매니페스트에 선언된 모든 프로바이더를 생성하고 그 onCreate()를 호출합니다. 바로 이 순서 하나 때문에 Firebase, WorkManager, AndroidX App Startup은 Application 클래스에 코드를 한 줄도 넣지 않고 스스로를 초기화할 수 있습니다. 읽기 경로에서는 ContentResolver 쿼리가 Binder 호출을 통해 행(row)을 복사해 오지 않습니다. 대신 고정 크기의 공유 메모리 영역인 CursorWindow로 뒷받침되는 Cursor를 돌려줍니다. 이 레슨을 끝까지 읽고 나면 다음을 할 수 있게 됩니다.

  • 앱 실행 시퀀스에서 ContentProvider.onCreate()Application.onCreate()에 대해 어디서 실행되는지 따라가 볼 수 있습니다.
  • installContentProviders가 매니페스트의 <provider> 항목을 어떻게 살아 있는 프로바이더 객체로 바꾸는지 설명할 수 있습니다.
  • AndroidX App Startup과 Firebase가 매니페스트 프로바이더를 자동 초기화 훅으로 어떻게 사용하는지, 그리고 그 비용이 무엇인지 설명할 수 있습니다.
  • ContentResolver 쿼리가 다른 프로세스의 프로바이더에 어떻게 도달하는지, 그리고 CursorWindow가 무엇인지 설명할 수 있습니다.
  • 아주 큰 테이블에 대한 쿼리가 왜 메모리를 고갈시키지 않는지, 그리고 왜 Cursor를 반드시 닫아야 하는지 설명할 수 있습니다.

앱 시작에서 프로바이더의 위치

시스템이 프로세스를 시작하면 ActivityThread.handleBindApplication이 애플리케이션 기동 시퀀스를 실행합니다. 관련 부분을 조금 다듬으면 다음과 같습니다.

// app = data.info.makeApplicationInner(data.restrictedBackupMode, null);
app = data.info.makeApplicationInner(data.restrictedBackupMode, null);
...
mInitialApplication = app;
...
// 제한 모드(restricted mode)에서는 프로바이더를 기동하지 않습니다.
// 프로바이더가 앱의 커스텀 Application 클래스에 의존할 수 있기 때문입니다.
if (!data.restrictedBackupMode) {
    if (!ArrayUtils.isEmpty(data.providers)) {
        installContentProviders(app, data.providers);
    }
}
...
mInstrumentation.callApplicationOnCreate(app);

여기서 핵심은 순서입니다. makeApplicationInnerApplication 하위 클래스를 생성하고 그 위에서 attachBaseContext()를 호출하므로, 다음 줄들이 실행될 즈음에는 Application 객체가 이미 존재하고 Context도 가지고 있습니다. 그런 다음 installContentProvidersdata.providers에 있는 모든 프로바이더를 기동하는데, 이 목록은 병합된 매니페스트를 바탕으로 시스템이 이 프로세스에 넘겨준 ProviderInfo들입니다. 그 일이 모두 끝난 다음에야 callApplicationOnCreate(app)이 실행되고, 이것이 결국 Application.onCreate()를 호출합니다.

그래서 생명주기는 이렇게 됩니다. Application 생성자와 attachBaseContext(), 그다음 선언된 각 프로바이더의 ContentProvider.onCreate(), 그다음 Application.onCreate(). 프로바이더의 onCreate()getContext()를 호출해 애플리케이션 Context를 얻을 수 있지만, Application.onCreate()에서 한 일이 이미 일어났다고 가정할 수는 없습니다.

주석에 드러난 예외가 하나 있습니다. 제한 백업 모드(restricted backup mode)에서는 프로바이더를 일부러 시작하지 않는데, 백업 모드가 인스턴스화하지 않는 커스텀 Application 클래스에 프로바이더가 의존할 수 있기 때문입니다.

매니페스트 항목에서 onCreate()까지

installContentProviders는 프로바이더 목록을 순회하면서 각각에 대해 installProvider를 호출하고, 그 결과를 Activity Manager에 게시(publish)합니다.

private void installContentProviders(Context context, List<ProviderInfo> providers) {
    final ArrayList<ContentProviderHolder> results = new ArrayList<>();
    for (ProviderInfo cpi : providers) {
        ContentProviderHolder cph = installProvider(context, null, cpi,
                false /*noisy*/, true /*noReleaseNeeded*/, true /*stable*/);
        if (cph != null) {
            cph.noReleaseNeeded = true;
            results.add(cph);
        }
    }
    ActivityManager.getService().publishContentProviders(getApplicationThread(), results);
}

installProvider는 프로바이더가 속한 패키지에 맞는 Context를 해석한 다음, 앱의 AppComponentFactory를 통해 리플렉션으로 클래스를 인스턴스화하고 연결합니다.

localProvider = packageInfo.getAppFactory().instantiateProvider(cl, info.name);
provider = localProvider.getIContentProvider();
...
localProvider.attachInfo(c, info);

onCreate()가 실제로 발화하는 곳은 attachInfo입니다. ContentProvider.attachInfo 안에서 Context를 저장하고 ProviderInfo로부터 권한과 authority를 읽은 뒤, 마지막 줄은 다음과 같습니다.

if (mContext == null) {
    mContext = context;
    ...
    setAuthorities(info.authority);
    ...
    ContentProvider.this.onCreate();
}

이 면접 질문은 구독자 전용입니다

Dove Letter를 구독하시면 안드로이드, 코틀린 개발 관련 독점 면접 질문의 전체 내용을 볼 수 있습니다.

구독하기
면접 질문 목록으로 가기