Xposed 모듈 업그레이드: LibXposed API 102 완벽 가이드
이 문서는 기존 XposedBridge API 54-93 기반의 전통적인 Xposed 모듈을 최신 LibXposed API 102(LSPosed 1.9.3+, Vector 등 현대 프레임워크)로 업그레이드하는 표준 절차, 코드 패러다임, 메타데이터 규칙 및 성능 최적화 전략을 정리합니다.
향후 다른 모듈에 대한 업그레이드 지침도 이 규칙을 따릅니다.
1. 핵심 변경 사항 비교
| 특성 / 차원 | 전통적인 Xposed (API 54-93) | 현대 LibXposed (API 102) |
|---|---|---|
| 종속성 좌표 | de.robv.android.xposed:api:82 (또는 93) | io.github.libxposed:api:102.0.0 |
| 진입 베이스 클래스/인터페이스 | IXposedHookLoadPackage, IXposedHookZygoteInit | io.github.libxposed.api.XposedModule 상속 |
| 진입 선언 | assets/xposed_init | META-INF/xposed/java_init.list |
| 모듈 속성 설정 | AndroidManifest.xml의 <meta-data> | META-INF/xposed/module.prop |
| 범위 선언 | xposedscope 리소스 배열 | META-INF/xposed/scope.list |
| Hook 프로그래밍 모델 | XC_MethodHook (before / after) 콜백 | OkHttp 스타일 체인 인터셉터 Hooker (chain.proceed()) |
| 핫 리로드 (Hot Reload) | 지원 안 함 (대상 앱 또는 시스템 재시작 필요) | 기본 지원 (onHotReloading / onHotReloaded 오버라이드) |
| Zygote 주입 | initZygote 전역 주입 지원 | Zygote 주입 금지, 범위 내 프로세스에만 주입 |
| 난독화/R8 지원 | Hook 메서드명과 클래스명 엄격히 보존 필요 | R8 난독화 기본 지원, 적절한 규칙만 선언하면 됨 |
2. 표준 마이그레이션 4단계
1단계: 빌드 및 종속성 마이그레이션 (build.gradle)
-
종속성 업데이트: 모듈의
build.gradle(예:app/build.gradle또는hook/build.gradle)에서 종속성을 교체합니다.dependencies { // 제거: compileOnly 'de.robv.android.xposed:api:82' // 현대 LibXposed 102 API 도입 compileOnly 'io.github.libxposed:api:102.0.0' compileOnly 'androidx.annotation:annotation:1.5.0' } -
Java 컴파일 호환성:
android { compileOptions { sourceCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11 } } -
ProGuard / R8 규칙 설정 (
proguard-rules.pro):-dontwarn io.github.libxposed.annotation.** -adaptresourcefilecontents META-INF/xposed/java_init.list -keep,allowoptimization,allowobfuscation public class * extends io.github.libxposed.api.XposedModule { public <init>(); }
2단계: 현대 메타데이터 파일 설정 (META-INF/xposed/)
모듈 소스 디렉터리 src/main/resources/META-INF/xposed/ 아래에 다음 파일을 생성합니다.
1. module.prop (모듈 메타데이터)
minApiVersion=102
targetApiVersion=102
autoHotReload=true
exceptionMode=protective
minApiVersion: 지원되는 최소 API 버전,102로 설정.targetApiVersion: 대상 API 버전,102로 설정.autoHotReload:true로 설정하면 모듈 업데이트 설치 시 자동으로 핫 리로드가 트리거됩니다.exceptionMode:protective(기본 보호 모드, Hook 예외가 호스트 앱을 크래시시키지 않음) 또는passthrough.
2. java_init.list (진입 클래스 선언)
모듈 진입 전체 클래스명을 한 줄로 선언:
com.example.myhook.MainHook
3. scope.list (권장 범위)
한 줄에 하나의 대상 패키지 이름을 지정하여 모듈이 기본적으로 주입할 앱을 선언합니다.
com.android.chrome
com.microsoft.emmx
com.example.targetapp
3단계: 매니페스트 파일 설정 (AndroidManifest.xml)
모듈의 AndroidManifest.xml을 업데이트합니다.
<application ...>
<!-- 기존 메타데이터 유지, 중요: xposedminversion을 102로 설정 -->
<meta-data
android:name="xposedmodule"
android:value="true" />
<meta-data
android:name="xposeddescription"
android:value="모듈 기능 설명" />
<meta-data
android:name="xposedminversion"
android:value="102" />
<meta-data
android:name="xposedscope"
android:resource="@array/xposed_scope" />
</application>
4단계: Hook 로직 재구성 (XposedModule)
1. 기본 템플릿과 생명주기
package com.example.myhook;
import android.util.Log;
import androidx.annotation.NonNull;
import io.github.libxposed.api.XposedInterface;
import io.github.libxposed.api.XposedModule;
public class MainHook extends XposedModule {
private static final String TAG = "MyHookModule";
private volatile boolean mHooked = false;
public MainHook() {
super();
}
/**
* 호스트 앱 ClassLoader가 준비되고 Application이 생성되기 직전에 트리거됩니다.
*/
@Override
public void onPackageReady(@NonNull PackageReadyParam param) {
if (mHooked) return;
synchronized (this) {
if (mHooked) return;
mHooked = true;
initHooks(param.getPackageName(), param.getClassLoader());
}
}
/**
* 핫 리로드 준비 콜백 (이전 버전 코드에서 실행)
* true를 반환하면 프레임워크가 새 버전으로 교체하도록 허용합니다.
*/
@Override
public boolean onHotReloading(@NonNull HotReloadingParam param) {
return true;
}
/**
* 핫 리로드 완료 콜백 (새 버전 코드에서 실행)
*/
@Override
public void onHotReloaded(@NonNull HotReloadedParam param) {
// 이전 버전에서 등록된 모든 Hook 해제
param.getOldHookHandles().forEach(XposedInterface.HookHandle::unhook);
synchronized (this) {
mHooked = false;
initHooks("hot-reload", getClass().getClassLoader());
}
}
private void initHooks(String packageName, ClassLoader classLoader) {
// 구체적인 Hook 로직 실행
}
}
2. 핵심 Hook 문법 대조
| 대상 작업 | 전통적인 Xposed 방식 | LibXposed 102 현대 방식 |
|---|---|---|
| 메서드 찾기 및 Hook | XposedHelpers.findAndHookMethod("클래스명", cl, "메서드", argTypes..., callback) | Method m = Clazz.class.getDeclaredMethod("메서드", argTypes...);hook(m).intercept(chain -> { ... }); |
| 원본 메서드 실행 | 기본적으로 직접 실행; param.getResult() 호출 | Object result = chain.proceed(); |
| 인자 수정 후 호출 | param.args[0] = newVal; | Object result = chain.proceed(new Object[]{ newVal, ... }); |
| 가로채기 및 반환값 교체 | param.setResult(customVal); | 사용자 정의 값 직접 반환 (chain.proceed() 호출하지 않음):hook(m).intercept(chain -> customVal); |
| 입력 인자 가져오기 | param.args[i] | chain.getArg(i) 또는 chain.getArgs() |
| this 인스턴스 가져오기 | param.thisObject | chain.getThisObject() |
| 생성자 Hook | findAndHookConstructor(...) | Constructor<?> ctor = ...;hook(ctor).intercept(chain -> ...); |
| 정적 초기화 블록 Hook | 전통적인 API에서 표준 지원 안 함 | hookClassInitializer(TargetClass.class).intercept(...) |
3. 고빈도 Hook 시나리오 및 성능 심층 최적화 패턴
고빈도 호출(예: isEnabled, View 드로잉, 터치 디스패치)에 대한 황금 최적화 규칙:
-
제로 오버헤드 고속 경로 (스니핑 패스스루): 메서드가 상태 조회 메서드(예:
isEnabled(),isLoaded())인 경우, 먼저 가벼운 네이티브 필드 읽기chain.proceed()를 호출합니다. 실제 상태가 거짓이면, 후속 로직과 스택 검사를 모두 건너뛰고 바로 거짓을 반환합니다. -
Android 14+ (API 34+)에서
StackWalker지연 순회 사용: 전통적인new Throwable().getStackTrace()는 전체 스레드 스택 깊이 확장(Native Unwind)과 수백 개의 문자열 할당을 유발하여 GC 드랍프레임을 쉽게 일으킵니다. Java 9 / Android 14+의java.lang.StackWalker를 사용하여 필요할 때만 계층별로 스캔하며, 전체 스택 할당이 없습니다. -
Android < 34에서 동적 스택 위치 찾기 (하드코딩 금지): 한 번만
new Throwable().getStackTrace()를 실행하고, 아래로 순회하며 대상 메서드가 속한 시스템 클래스(예:AccessibilityManager)를 찾은 후, 바로 다음 스택 프레임이 실제 호출자입니다.[3]또는[4]와 같은 하드코딩을 절대 사용하지 않아 LSPosed 버전 간 안정적인 실행을 보장합니다.
성능 최적화 예시 코드:
// Android 14+ StackWalker 확인
private static final boolean HAS_STACK_WALKER;
static {
boolean has;
try {
Class.forName("java.lang.StackWalker");
has = (Build.VERSION.SDK_INT >= 34);
} catch (Throwable t) {
has = false;
}
HAS_STACK_WALKER = has;
}
// 고빈도 메서드 Hook 예시
Method isEnabled = AccessibilityManager.class.getDeclaredMethod("isEnabled");
hook(isEnabled).intercept(chain -> {
Object real = chain.proceed();
if (Boolean.FALSE.equals(real)) {
return false; // 시스템이 이미 꺼져 있음, 제로 오버헤드 직통 반환
}
return isCallerWhitelisted() ? real : false;
});
private static boolean isCallerWhitelisted() {
if (HAS_STACK_WALKER) {
try {
return Api34StackWalker.check();
} catch (Throwable ignored) {}
}
return checkLegacy();
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
private static final class Api34StackWalker {
private static final StackWalker WALKER = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
static boolean check() {
return Boolean.TRUE.equals(WALKER.walk(frames -> {
boolean foundTarget = false;
var it = frames.iterator();
while (it.hasNext()) {
var f = it.next();
if ("android.view.accessibility.AccessibilityManager".equals(f.getClassName())) {
foundTarget = true;
} else if (foundTarget) {
return isPackageAllowed(f.getClassName());
}
}
return false;
}));
}
}
4. 업그레이드 흔한 함정 및 문제 해결 체크리스트
assets/xposed_init를 계속 유지해야 하나요?- 현대 API 102는
META-INF/xposed/java_init.list만 읽습니다.assets/xposed_init를 유지하면 구형 프레임워크가 전통적인 리플렉션 방식으로 로드하려고 시도하여 오류가 발생할 수 있습니다. 제거하거나 비워 두는 것이 좋습니다.
- 현대 API 102는
compileSdk와 Android SDK 플랫폼:- 최신 인터페이스와 Java 11/17 언어 기능을 원활하게 사용하려면
compileSdk와targetSdk를 Android 33 또는 34로 업그레이드하는 것이 좋습니다.
- 최신 인터페이스와 Java 11/17 언어 기능을 원활하게 사용하려면
- Gradle 호환성:
- 빌드 환경이 JDK 17/21을 사용하는 경우, Gradle Wrapper를
7.4+(권장7.5.1또는8.x)로, AGP를7.3+로 업그레이드해야 합니다.
- 빌드 환경이 JDK 17/21을 사용하는 경우, Gradle Wrapper를
- 모듈 인식 확인:
- 압축 해제 도구를 사용하여 최종 APK 루트 디렉터리에 다음 파일이 있는지 확인하세요:
META-INF/xposed/module.propMETA-INF/xposed/java_init.listMETA-INF/xposed/scope.list
- 압축 해제 도구를 사용하여 최종 APK 루트 디렉터리에 다음 파일이 있는지 확인하세요: