ㅈㅅㄹ

밥 벌어 먹다 보면, 소위 말하는 어른의 사정이라는 것 때문에 별 해괴한 짓들을 하게 되곤 하는 데, DexClassLoader를 사용하여 다른 패키지의 클래스를 로드해서 쓰는 방법이 그들 중 하나이다. 다른 패키지의 클래스를 로딩하는 데 딱히 특별한 제약 사항이 있는 건 아니므로 심지어는 preload 된 패키지의 클래스도 읽어서 쓸 수 있다.


DexClassLoader를 사용해서 어떤 식으로 패키지를 읽어야 하나 살펴 보기 전에 우선 생성자가 어떻게 생겼는지 들여다 보자.


Public Constructors
DexClassLoader(String dexPath, String dexOutputDir, String libPath, ClassLoader parent)
Creates a DexClassLoader that finds interpreted and native code.


첫 번째 인자는 읽어들일 jarapk파일의 위치를 나타낸다. 만약 우리가 SD카드에 들어 있는 ATestApp.apk라는 이름의 패키지에서 특정 클래스를 읽어들이길 바란다면 "/sdcard/ATestApp.apk"를 첫 번째 인자로 넘겨주어야 한다. 물론 실제로는 이렇게 SD 카드에 APK 올려 놓고 사용하는 일은 당연히 없겠지만 일단은 이렇다고 가정하자. 두 번째 인자로는 첫 번째 인자로 주어진 APK 내의 classes.dex 파일을 최적화 하여 보관해 둘 임시 폴더의 위치이다. 세 번째 인자는 Native 라이브러리의 탐색 경로, 네 번째는 부모 클래스 로더를 지정해 준다.


DexClassLoader를 생성했다면 그 다음은 일반적인 Java의 ClassLoader를 사용하는 방식과 동일하다. ATestApp.apkorg.example.test.a.MyTestClass 클래스의 인스턴스를 하나 생성하기 위해선,

DexClassLoader loader = 
    new DexClassLoader("/sdcard/ATestApp.apk", "/sdcard", null,
                       getClass().getClassLoader());
Class<?> cls = null;
Object obj = null;
try {
    cls = loader.loadClass("org.example.test.a.MyTestClass");
    // MyTestClass has a constructor with no arguments
    Constructor<?> cons = cls.getConstructor(); 
    obj = cons.newInstance();
    Method m = cls.getMethod("myMethod", String.class);
    m.invoke(obj, "Hello");
}
catch (Throwable e) {
    e.printStackTrace();
}

과 같이 사용하면 된다. 생성한 instance의 특정 메쏘드에 접근하기 위해선, cls에 getMethod()getMethods() 등을 사용하여 Method 인스턴스를 가져 온 후 invoke()를 사용하자.


그럼 이걸 언제 써야 하느냐에 대해선 사실 정답은 없다. 만약 어느 한 쪽 패키지에서 JNI를 통해 native 라이브러리에서 기능 전반을 구현한 것이 있고, 이걸 양쪽 패키지에 따로따로 올리기엔 곤란한 상황이라면 DexClassLoader를 사용한 클래스 끌어다 쓰기는 해답이 될 수도 있다. 아니면 A라는 패키지에 포함된 특정 클래스의 기능이 안드로이드 기본 Java Heap 16M 정도는 가볍게 전부 먹어치울 정도로 무거울 경우, B 패키지에서 동일 기능을 Intent로 요청하면 A 패키지의 Java Heap을 공유하게 되어서 문제가 된다면 이 방법이 대안이 될 수도 있겠다. 여러가지 사용법이 있긴 하지만 딱히 "우와 이거면 만사 해결!" 뭐 이런 감흥이 생기는 사용처는 내 상상력의 부족인지 마땅히 없다.


앞선 예제에서는 비현실적으로 SD 카드에 APK를 적재해 놓고 사용하는 방식을 썼지만 실제로는 전혀 현실성이 없는 방식이다. 따라서 이미 단말에 설치된 다른 패키지의 클래스를 읽어 들어야 하는 상황이 보다 현실적이라 할텐데, 클래스 로더 생성 시 APK 파일의 경로를 정확히 지정해 주어야 하나 그 경로를 Guessing할 수 밖에 없다는 제약이 있다.


기본 안드로이드 소스에서 별 다른 수정 사항이 없는 단말이라면 시스템에 설치한 (preload 된 것이 아닌) 어플리케이션의 패키지 APK 파일은 /data/app 아래에 따로 보관 된다(preload 된 것들은 /system/app). 문제는 이 파일시스템 영역이 상당히 제한적인 것이다. /data/app 아래에선 디렉토리내의 파일 목록을 가져 올 수 없다. 불행중 다행으로 /data/app 아래에 보관되는 APK 파일은 해당 어플리케이션의 패키지명을 그 키로 가지고 있어서 어느 정도는 실제 보관된 파일명을 추측할 수는 있으나 완전하게 확신하거나 시스템에 그것에 대해 질의할 수 있는 수단이 없다. 민감한 데이터가 들어 있는 부분이므로 실제 상용 단말에서는 디렉토리에 파일 목록을 요청 해봤자 빈 목록만 날아오도록 되어 있다. (예전에 안드로이드 파티션 별 권한에 대한 관련 다큐먼트를 봤었는데 링크를 못찾겠네 -_-)


2.1 소스 (적어도 일부)에선 org.example.test.a 라는 패키지 명을 가진 어플리케이션 패키지는 /data/app/org.example.test.a.apk 라는 이름으로 저장될 것이다. 그러나 2.2 기반 단말에서는 /data/app/org.example.test.a-#.apk(#는 숫자) 라는 이름으로 저장된다. 파일이 존재하는 지 여부는 귀납적으로 도출한 경로를 지정해 주고 실제로 읽어 보는 수 밖에 없다. 결국 차후에 /data/app 아래에 APK 파일을 보관하는 정책이 달라지거나 혹은 단말사의 커스터마이징으로 인해, 시스템에 설치된 앱의 APK파일을 읽어 들일 수 없는 경우를 마주하게 될 잠재적 불안 요소를 안고 갈 수 밖에 없게 되는 것이다.


안드로이드에선 Intent라는 훌륭한 어플간 인터페이스를 제공하고 있다. 어지간하면 플랫폼이 권장하는 걸 그냥 그대로 사용하는 편이 정신 건강상 이롭다. DexClassLoader에 대해선, 그냥 이런 방법도 있구나 하는 정도로만 알아두자.