ArturoYi
安卓代码加固
宿主只 implementation hidden-sdk。landing-page 对 SDK 必须 compileOnly,明文 class 不准进入 APK。

即使已经加密,只要宿主使用 implementation(project(":hidden-landing-page")),明文 class 仍会进入 APK。加固的第一道检查是 依赖图,不是算法。

:hidden-sdk 只作为 d8 的 --lib,接口留在主 DEX。实现进入加密 dex。两边不要互相 implementation。

模块边界

settings.gradle.kts
include(":app")
include(":hidden-sdk")
include(":hidden-landing-page")
include(":ads")
写法结果
宿主 implementation(hidden-sdk)正确。接口在主 dex,实现在 AAR assets
宿主 implementation(hidden-landing-page)明文 class 进入 APK
landing-page implementation(:hidden-sdk)SDK 类打进加密 dex,体积变大且无必要
landing-page compileOnly(:hidden-sdk)本仓库内部正确
宿主 import com.example.minidex.hidden编译期把实现引用写进主 dex

强制 compileOnly

landing-page 模块的头注释和依赖固定了方向:

hidden-landing-page/build.gradle.kts
// 不要被宿主以 implementation / api / runtimeOnly 依赖,否则明文会进 APK。
// 对 :hidden-sdk 只能 compileOnly:接口进宿主,实现进加密 dex。
dependencies {
    compileOnly(project(":hidden-sdk"))
}

它还需要 SDK 里的 HiddenIncomingKeepAlive。Service 和 Receiver 必须留在主 DEX,供系统拉起。compileOnly 让编译期能解析这些符号,运行时由宿主 ClassLoader 提供,class 不会打进 jar。

解析期再加一层检查:运行时相关的 configuration 里如果出现 :hidden-sdk,直接失败:

hidden-landing-page/build.gradle.kts
configurations.configureEach {
    val runtimeLike = configurationName.contains("Implementation") ||
        configurationName.contains("Runtime") ||
        configurationName == "api" ||
        configurationName.endsWith("Api")
    if (!runtimeLike) return@configureEach
    incoming.afterResolve {
        check(leaked.isEmpty()) {
            ":hidden-landing-page 把 :hidden-sdk 放进了 $configurationName。必须 compileOnly。"
        }
    }
}

:app 做对称检查:任何 runtime configuration 解析到 :hidden-landing-page 就失败。

禁止源码 import

即使没有写 Gradle 依赖,只要有人把实现类拷进 app 源码并 import,主 dex 仍会留下类描述符。preBuild 会扫描全部 kt/java 文件:

app/build.gradle.kts
tasks.register("verifyAppDoesNotImportHiddenLandingPage") {
    doLast {
        val forbidden = Regex("""import\s+com\.example\.minidex\.hidden\b""")
        check(offenders.isEmpty()) {
            ":app 不能 import hidden-landing-page,请走 HiddenSdk.invoke。"
        }
    }
}
tasks.named("preBuild").configure {
    dependsOn("verifyAppDoesNotImportHiddenLandingPage")
}

宿主只允许 com.example.minidex.sdk.*。

保留必须 keep 的入口

AAR 带有 consumer-rules.pro。宿主开启 R8 时,不能把 JNI 入口和系统要反射的占位类 shrink 掉:

hidden-sdk/consumer-rules.pro
-keep class com.example.minidex.sdk.HiddenSdk { *; }
-keep class com.example.minidex.sdk.HiddenInitResult { *; }
-keep class com.example.minidex.sdk.HiddenInitResult$Code { *; }
-keep class com.example.minidex.sdk.internal.HiddenIncomingKeepAlive { *; }
-keep class com.example.minidex.sdk.internal.HiddenIncomingKeepAliveService { *; }
-keep class com.example.minidex.sdk.internal.HiddenIncomingAlarmReceiver { *; }
-keep class com.example.minidex.sdk.internal.HiddenDecrypt { *; }
-keep class com.example.minidex.sdk.internal.HiddenCrypto { *; }

HiddenSdk 的 nativeInit / nativeDispatch 按类名 + 方法名绑定。R8 改名后 so 会 UnsatisfiedLinkError。KeepAlive 相关类由系统 Intent 拉起,也必须留名。

这些规则是为了保留入口,不是把实现 keep 进主 DEX。实现类本来就不在 classes.jar 里。

复用时不要拆 AAR

其他工程 不要 include(":hidden-landing-page"),也不要解压 AAR 再单独拷贝 payload。

dependencies {
    implementation(files("libs/hidden-sdk-1.0.0.aar"))
}

下一步

Copyright © 2026