包体积
background:在大型APP性能优化中,包体积一直都是个比较让人头疼的事,做到后期需要的技术难度极高,但是收益却微乎其微...言归正传,虽然在我们打包时经过ProGuard等工具的编译优化,将会去除掉绝大多不被使用到的代码,但总有些代码模块随着不断的迭代功能已经下线,但是代码还是被带到了线上,最终造成apk文件增大。现针对这个问题现提出方案:在运行时监控类的使用情况,基于线上大量用户真实情况,反推出现有的废弃代码及模块,最后下掉这些无用代码及模块,达到减少包体积的的终极目标。
一、覆盖率采集的常见方案
本文所说的代码覆盖率均指类维度的代码覆盖率。
- 基于插桩
插桩的方式:编译时期在每个类(通常过滤系统类)插入static代码块,用于标记该class被JVM加载(使用过)。字节开源的bytex插件平台中的coverag-plugin插件就是基于此种方式统计线上代码覆盖率。但这种方式有几个致命的弊端:1.插桩本身会增大包体积,测试时发现插入15万个类的cinit,将会增大约3MB的包体积。2.由于插桩的类在首次被加载的时候都会执行标记逻辑,这会导致APP的性能劣化,特别是启动时的性能劣化
- Hack classTable
高德和阿里互娱应该是采用的这种方案采集线上代码覆盖率。classTable变量是Android7之后art虚拟机在ClassLoader.java中加入的成员属性,对应的具体定义在art/runtime/class_table.h中,在Java层classTable对应的只是个内存地址,所有通过ClassLoader加载的Class将会被保存到classTable。通过hack这个classTable,通过其提供的Visit函数能够判断这个类是否被加载过。但这种方案也有一个弊端就是实现相对比较复杂
二、基于JVMTI做线上覆盖率采集
1. JVMTI简介(ART TI)
1)简介
JVMTI(JVM Tool Interface)是 Java 虚拟机所提供的 Native 编程接口。通过这些接口,开发人员不仅可以调试在虚拟机上运行的 Java 程序,还能查看它们运行的状态、控制环境变量,甚至修改代码逻辑,从而帮助开发人员监控和优化程序性能。但在Android(ART)上被Google官方在Release模式中禁用掉了,如果需要使用则需要hack,同时对于系统版本有一定要求(>=Android8)。下图是Google官方对于ART TI的概念图:
- APP process:我们需要监控的APP进程
- Agent代理:需要我们自己实现定制的代理
- ART/Core:ART虚拟机
- External process:外部进程eg:Android studio profiler
图中的JVMTI Plugin的具体实现是libopenjdkjvmti.so,Android SDK 30以上的手机在apex/com.android.art/lib64/目录下
2)JVMTI能干什么
下面大概介绍一下JVMTI能做哪些黑科技:
- 重定义 Class
- 跟踪对象分配和垃圾回收过程
- 遵循对象的引用树,遍历堆中的所有对象
- 检查 Java 调用堆栈
- 暂停和恢复所有线程
- 其他功能
JVMTI主要通过我们注册的一些回调监听通知相应信息,比如监听线程启动、结束。监听类加载的过程等,而在这些回调中,我们能够做一些黑科技操作,比如在ClassFileLoadHook回调时,我们能拿到将要加载进JVM的class并做出相应修改,实现插桩、类文件修改等操作。JVMTI能做的东西非常多,本次我们主要通过JVMTI实现线上无用类的采集功能。 能够注册的回调:
c++ 体验AI代码助手 代码解读复制代码typedef struct {
/* 50 : VM Initialization Event */
jvmtiEventVMInit VMInit;
/* 51 : VM Death Event */
jvmtiEventVMDeath VMDeath;
/* 52 : Thread Start */
jvmtiEventThreadStart ThreadStart;
/* 53 : Thread End */
jvmtiEventThreadEnd ThreadEnd;
/* 54 : Class File Load Hook */
jvmtiEventClassFileLoadHook ClassFileLoadHook;
/* 55 : Class Load */
jvmtiEventClassLoad ClassLoad;
/* 56 : Class Prepare */
jvmtiEventClassPrepare ClassPrepare;
/* 57 : VM Start Event */
jvmtiEventVMStart VMStart;
/* 58 : Exception */
jvmtiEventException Exception;
/* 59 : Exception Catch */
jvmtiEventExceptionCatch ExceptionCatch;
/* 60 : Single Step */
jvmtiEventSingleStep SingleStep;
/* 61 : Frame Pop */
jvmtiEventFramePop FramePop;
/* 62 : Breakpoint */
jvmtiEventBreakpoint Breakpoint;
/* 63 : Field Access */
jvmtiEventFieldAccess FieldAccess;
/* 64 : Field Modification */
jvmtiEventFieldModification FieldModification;
/* 65 : Method Entry */
jvmtiEventMethodEntry MethodEntry;
/* 66 : Method Exit */
jvmtiEventMethodExit MethodExit;
/* 67 : Native Method Bind */
jvmtiEventNativeMethodBind NativeMethodBind;
/* 68 : Compiled Method Load */
jvmtiEventCompiledMethodLoad CompiledMethodLoad;
/* 69 : Compiled Method Unload */
jvmtiEventCompiledMethodUnload CompiledMethodUnload;
/* 70 : Dynamic Code Generated */
jvmtiEventDynamicCodeGenerated DynamicCodeGenerated;
/* 71 : Data Dump Request */
jvmtiEventDataDumpRequest DataDumpRequest;
/* 72 */
jvmtiEventReserved reserved72;
/* 73 : Monitor Wait */
jvmtiEventMonitorWait MonitorWait;
/* 74 : Monitor Waited */
jvmtiEventMonitorWaited MonitorWaited;
/* 75 : Monitor Contended Enter */
jvmtiEventMonitorContendedEnter MonitorContendedEnter;
/* 76 : Monitor Contended Entered */
jvmtiEventMonitorContendedEntered MonitorContendedEntered;
/* 77 */
jvmtiEventReserved reserved77;
/* 78 */
jvmtiEventReserved reserved78;
/* 79 */
jvmtiEventReserved reserved79;
/* 80 : Resource Exhausted */
jvmtiEventResourceExhausted ResourceExhausted;
/* 81 : Garbage Collection Start */
jvmtiEventGarbageCollectionStart GarbageCollectionStart;
/* 82 : Garbage Collection Finish */
jvmtiEventGarbageCollectionFinish GarbageCollectionFinish;
/* 83 : Object Free */
jvmtiEventObjectFree ObjectFree;
/* 84 : VM Object Allocation */
jvmtiEventVMObjectAlloc VMObjectAlloc;
} jvmtiEventCallbacks;
本次的代码覆盖率采集功能使用jvmti提供的一个函数GetLoadedClasses
c 体验AI代码助手 代码解读复制代码jint classCount;
jclass *classes;
mJvmtiEnv->GetLoadedClasses(&classCount, &classes);
继续追踪堆栈发现,这个函数其实也是访问classTable变量,只不过jvmti-plugin内部帮我们封装好了,所以我们使用只需要调用接口就行了
2. 在Android上使用JVMTI的技术难点
由于JVMTI功能十分强大,可能会有各种不同的功能需要用到JVMTI的功能,比如线上代码覆盖率、内存监控、崩溃堆栈分析等,所以我将JVMTI封装为一套SDK——>JVMTI-SDK,整体设计如下:
整个SDK大概分为4层:
- Native层 负责与JVMTI-Plugin直接通信,且同时向Core层提供相应hack、回调接口等相应能力
- Core层(Core) JVMTI核心基础层,下与Native通信,向上提供JVMTI基础能力,同时集成基础SDK
- 功能层(Ability) 主要提供特定方向的功能,如无用类检测,能够使用Core层提供的JVMTI基础能力实现特定方向的复杂功能
- 整合层(Integration) 直接被宿主依赖,暴露对外提供的接口。同时整合SDK功能,对各种功能进行统一控制
从代码结构来看,每一层都是一个module,本次的无用类检测属于功能层:
由于Google在ART虚拟机中对TI功能进行了限制,必须要Debug模式才能使用TI能力,但我们是需要在线上使用该功能,所以突破Google对于Release包TI的封锁是必然的。 attachAgent的源码流程如下(借用图):
1)hack IsJdwpAllowed
当我们尝试在Release模式直接加载Agent代理时,程序会发生崩溃抛出异常java.lang.SecurityException: Can't attach agent, process is not debuggable. 在dalvik_system_VMDebug.cc中的void VMDebug_nativeAttachAgent发现首次被拦截的地方
c 体验AI代码助手 代码解读复制代码static void VMDebug_nativeAttachAgent(JNIEnv* env, jclass, jstring agent, jobject classloader) {
if (agent == nullptr) {
ScopedObjectAccess soa(env);
ThrowNullPointerException("agent is null");
return;
}
if (!Dbg::IsJdwpAllowed()) {
ScopedObjectAccess soa(env);
ThrowSecurityException("Can't attach agent, process is not debuggable.");
return;
}
std::string filename;
{
ScopedUtfChars chars(env, agent);
if (env->ExceptionCheck()) {
return;
}
filename = chars.c_str();
}
Runtime::Current()->AttachAgent(env, filename, classloader);
}
IsJdwpAllowed这个值是debugger.cc的一个静态变量,VMDebug_nativeAttachAgent函数是通过debugger.cc中国的gJdwpAllowed变量来判断,这里首先我们需要hack这个值,由于是静态变量,所以相对比较简单,查询Dbg::setJdwoAllowed的符号:_ZN3art3Dbg14SetJdwpAllowedEb(64位),由于Android7以上对于系统库的dlopen等函数做了限制,所以这里也需要绕过,如dlfunctions、xdl
2)hack is_java_debuggable_属性
上面对于gJdwpAllowed的hack完成之后,发现程序运行不崩溃了,但是还是有问题,c层Agent中拿到的jvmtiEnv指针为nullptr,attachAgent没有成功,我们继续分析源码
根据第一行系统log,找到对应的报错位置
最终跟到OpenjdkJvmTI.cc中的IsFullJvmtiAvailable
发现是
is_java_debuggable_是runtime的成员属性,hack这个值,需要拿到runtime对象,我们在Agent中可以通过JVM拿到唯一的runtime对象,然后跟上面一样的方式查SetJavaDebuggable函数符号_ZN3art7Runtime17SetJavaDebuggableEb(64位),拿到引用后调用函数修改is_java_debuggable_的值
c 体验AI代码助手 代码解读复制代码class Runtime {
public:
...
// Whether Java code needs to be debuggable.
bool is_java_debuggable_;
bool IsJavaDebuggable() const {
return is_java_debuggable_;
}
void SetJavaDebuggable(bool value);
...
}
经过如上操作Agent就能够attach成功了,能够如愿通过函数GetLoadedClasses拿到JVM中已经加载过的类信息,然后将这些类信息透传到Java层做处理(过滤、压缩等),然后上报后台
3)数据上传
数据的上传也大有讲究,线上全量后数据是非常大的,尽管是本地做缓存,上报压缩过滤后的增量数据,这块的成本也是非常大的,一个可行的方案是,在测试阶段将数据采集后上报后台,后台根据版本信息将这部分信息记录下来,在正式版本上线后通过CDN方式拉取这个缓存文件,且固定一个时间同步这个文件(从新拉取),这样的话,数据至少能减少80%,因为测试阶段基本会覆盖APP主链路,用户就不用从新上报这部分数据。本文暂不深究这块
3. 性能影响
使用JVMTI采集和上报数据,由于对于类的采集不是通过注册监听的方式,而是使用jvmti直接提供的接口,我们主动调用,还能控制采集间隔,所以理论上性能是几乎无影响的,下图是使用该方案在大型APP上的性能分析(Google pixel 4xl debug包):由此可见该方案的对于性能的影响可以称得上是无损方案(CPU的使用率增大其实是上报数据部分做本地diff导致,这部分可以优化,也建议使用idelHandler)
4. 兼容性适配
由于涉及到虚拟机ART hook,再加上Android8才支持JVMTI 1.2版本,所以会有兼容性风险,事实也确实如此
- Android8+的系统才能使用,且Android8的Debug.java不提供attachAgent的方法,Android9才提供,Android8的系统需要反射调用
- Android各系统的
libart.so的文件目录会不同,这里需要适配,适配以下几个目录
- RuntimeDebugState 适配
上述方案在线上灰度的时候,发现三星的某类机型、OPPO的某类机型会在Android13有崩溃情况,首先怀疑是厂商定制了ROM,但是后来在Google pixel的Android14上也有相应崩溃,基本排除厂商定制ROM,排查发现是jvmtiEnv指针为空导致的,进一步排查发现是由于hack is_java_debuggable_这个Runtime成员属性的时候失败了。考虑到之前的版本没有问题,但是Android14上有问题,所以猜测可能是Android14更改了Runtime结构,导致hack失效。追踪最新的Android14源码:(不想跟着追源码直接看最后的结论)
frameworks/base/core/java/android/os/Debug.java
libcore/dalvik/src/main/java/dalvik/system/VMDebug.java
art/runtime/native/dalvik_system_VMDebug.cc
这里发现IsJdwpAllowed这个hack点没有变化,继续去Runtime的AttachAgent art/runtime/runtime.cc
看到这里发现已经变了,
isJavaDebuggable居然不是判断的is_java_debuggable_这个成员属性的值了,而是一个枚举类型,但是仍然看不出问题,因为这里并没有被拦截,继续追踪,就要浮出水面了
跟踪到plugin的Load函数里
art/openjdkjvmti/OpenjdkJvmTi.cc到JVMTI-Plugin的初始化函数
GetEnvHandler
IsFullJvmtiAvailable()
OK了大功告成
结论:Android高系统版本更改了Runtime结构,删除了is_java_debuggable_属性,所以上面对于此的hack将在Android高版本失效,增加了RuntimeDebugState枚举类型来代替之前的bool类型,其实是增加了一种状态,之前的bool表示不了3种状态,将之前的hack在高版本替换为kJavaDebuggableAtInit即可,看runtime的初始化代码发现,默认值为kNonJavaDebuggable
c 体验AI代码助手 代码解读复制代码class Runtime {
public:
...
enum class RuntimeDebugState {
// This doesn't support any debug features / method tracing. This is the expected state usually.
kNonJavaDebuggable,
// This supports method tracing and a restricted set of debug features (for ex: redefinition
// isn't supported). We transition to this state when method tracing has started or when the
// debugger was attached and transition back to NonDebuggable once the tracing has stopped /
// the debugger agent has detached..
kJavaDebuggable,
// The runtime was started as a debuggable runtime. This allows us to support the extended set
// of debug features (for ex: redefinition). We never transition out of this state.
kJavaDebuggableAtInit
};
// Whether Java code needs to be debuggable.
RuntimeDebugState runtime_debug_state_;
bool IsJavaDebuggable() const {
return runtime_debug_state_ == RuntimeDebugState::kJavaDebuggable ||
runtime_debug_state_ == RuntimeDebugState::kJavaDebuggableAtInit;
}
bool IsJavaDebuggableAtInit() const {
return runtime_debug_state_ == RuntimeDebugState::kJavaDebuggableAtInit;
}
EXPORT void SetRuntimeDebugState(RuntimeDebugState state);
...
}
跟上面的hack方式类似,直接拿到高系统版本的libart.so文件,查SetRuntimeDebugState这个函数的符号:_ZN3art7Runtime20SetRuntimeDebugStateENS0_17RuntimeDebugStateE(64位),在AttachAgent之前将runtime中的这个值设置为kJavaDebuggableAtInit。OK适配结束,为了防止后续版本继续改runtime,导致之前的hack失效引发线上crash,建议增加hack失败的检查作为兜底,确保线上的稳定性。
作者:ZJcoming
链接:juejin.cn/post/734606…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
内存篇
JVM TI可以用来监控APP的哪些性能指标
1. 内存使用与垃圾回收
-
堆内存与非堆内存监控
通过JVM TI可实时获取堆内存各区域(如Eden、Survivor、老年代)的使用量、峰值及内存池分布情况5
6
7
。例如,检测内存泄漏时,可追踪对象分配路径并分析未被回收的对象引用链
10
。
-
垃圾回收(GC)活动
监控GC频率、耗时(如Young GC和Full GC的停顿时间)、回收的内存量,以及GC算法对应用性能的影响2
5
6
。例如,通过
GarbageCollectionStart和GarbageCollectionFinish事件记录GC周期10
。
2. 线程状态与并发问题
-
线程生命周期与状态
捕获线程的创建、启动、阻塞和终止事件,统计活跃线程数及状态分布(如RUNNABLE、BLOCKED、WAITING)3
4
10
。例如,通过
ThreadStart和ThreadEnd回调跟踪线程动态。 -
死锁检测与线程资源竞争
分析线程持有的锁及等待链,定位因锁竞争导致的性能瓶颈2
4
。例如,通过
MonitorContendedEnter事件发现高并发场景下的锁争用问题10
。
3. 类加载与代码执行
-
类加载与卸载统计
监控类加载次数、卸载数量及加载耗时,帮助识别类加载器泄漏或重复加载问题3
7
10
。例如,通过
ClassLoad和ClassPrepare事件追踪类加载过程。 -
方法执行耗时分析
利用MethodEntry和MethodExit回调记录方法调用时间,定位高耗时方法7
10
。结合代码覆盖率统计(如网页7提到的类使用监控),可进一步优化无用代码
7
。
4. CPU与系统资源
-
CPU使用率与上下文切换
监控应用占用的CPU时间片、用户态与内核态CPU消耗,以及线程上下文切换频率2
5
6
。例如,结合线程状态分析CPU密集型任务对性能的影响。
-
I/O与网络资源
间接关联磁盘读写和网络传输延迟(需结合系统级监控工具)9
,但JVM TI可通过对象分配追踪I/O相关资源占用
10
。
5. 动态代码修改与调试
-
代码热替换(HotSwap)
支持在运行时重定义类(RedefineClasses),用于动态修复代码缺陷或注入监控逻辑10
。例如,通过
ClassFileLoadHook修改字节码以实现无侵入性能采样7
。
-
断点与单步调试
设置断点(Breakpoint)并触发单步执行事件(SingleStep),用于复杂问题的现场诊断10
。
注意事项与限制
-
Android平台限制
JVM TI在Android中默认仅支持可调试应用(android:debuggable=true),但可通过反射或自定义Agent绕过部分限制10
。
-
性能开销控制
高频事件(如方法进入/退出)可能产生较大性能损耗,需通过采样或过滤机制优化7
10
。
-
兼容性
ART TI功能在Android 8.0及以上版本逐步完善,低版本需依赖特定Hack方案7
10
。
应用场景示例
-
内存泄漏排查:结合对象分配跟踪(
ObjectAlloc事件)和GC日志,定位未释放对象5
10
。
-
线上性能分析:通过无侵入Agent(如网页7的代码覆盖率方案)统计类使用情况,优化包体积
7
。
-
高并发调优:分析线程池利用率及锁竞争,优化线程配置
2
4
。
综上,JVM TI为深度性能分析提供了底层能力支持,但需结合实际场景选择合适的监控策略和工具链(如结合Prometheus、SkyWalking等平台实现可视化
1
5
6
)。
app怎么监控binder调用
基于搜索结果,监控Android应用的Binder调用可通过以下方法实现:
1. Java层动态代理监听(适用Android 10+)
-
ProxyTransactListener机制
Android 10引入的隐藏APIBinder.ProxyTransactListener允许在Binder调用前后触发回调。通过动态代理创建监听实例并注入到BinderProxy的stransactListener属性,可全局监控Java层Binder调用1
。
- 实现步骤:
- 绕过Hidden API限制(如通过Native线程JNI调用
VMRuntime.setHiddenApiExemptions加白)。 - 动态代理
ProxyTransactListener接口,记录调用耗时、线程堆栈等信息。
- 绕过Hidden API限制(如通过Native线程JNI调用
- 局限性:
-
无法获取Binder调用的数据参数(如
Parcel内容)1
。
-
仅支持Android 10及以上版本。
-
- 实现步骤:
2. JNI Hook拦截transactNative方法(全版本兼容)
-
Native函数替换
Binder调用最终会进入BinderProxy.transactNative这一JNI方法。通过JNI Hook替换其底层Native函数,可捕获完整的调用参数和返回结果1
3
。
- 关键技术点:
- 定位原Native函数:通过反射获取
BinderProxy.transactNative对应的ArtMethod指针。 - 函数替换:使用
RegisterNatives覆盖原JNI实现,插入自定义监控逻辑(如记录code、data、flags)。
- 定位原Native函数:通过反射获取
- 优势:
-
支持全Android版本,稳定性高
1
。
-
可解析
Parcel数据中的服务名、参数等详细信息3
。
-
- 关键技术点:
3. 内核层eBPF监控(系统级追踪)
-
基于binder_transaction的eBPF探针
在Linux内核中,Binder调用通过binder_ioctl和binder_transaction实现。通过eBPF挂载kprobe到这些函数,可捕获跨进程通信的原始数据2
。
- 数据解析:
-
提取
binder_transaction结构体中的target(目标服务)、code(操作码)、data(参数缓冲区指针)。 -
结合进程上下文过滤目标应用包名
2
。
-
- 适用场景:
- 分析恶意应用的系统服务滥用行为(如短信/定位窃取)。
- 需Root权限或自定义内核模块支持。
- 数据解析:
4. 日志增强与系统源码修改(需系统权限)
-
激活BinderProxy的ALOGV日志
在AOSP源码中,BinderProxy.transactNative内部包含ALOGV日志输出,但默认在User版本中关闭。通过以下修改可启用日志3
:
- 在
log.h中强制定义LOG_NDEBUG=0以开启VERBOSE日志。 - 添加PID过滤逻辑,仅打印目标进程的Binder调用。
- 重新编译系统或使用Xposed模块动态注入。
- 在
方案选型建议
需求场景
推荐方案
优缺点
线上卡顿监控(无Root)
Java动态代理 + JNI Hook
低侵入性,但需处理Hidden API限制
1
安全分析(需参数级监控)
eBPF + 内核探针
数据全面,依赖Root或定制ROM
2
系统调试(有源码权限)
日志增强 + 编译优化
直接获取调用链,需重新部署系统
3
注意事项
- 性能影响:高频Binder调用场景(如频繁跨进程通信)需限制监控粒度,避免引发性能劣化。
- 兼容性风险:JNI Hook和内核探针可能因Android版本升级失效,需持续适配。
- 隐私合规:监控涉及跨进程数据时需符合隐私法规(如GDPR、个人信息保护法)。
通过以上方案,开发者可根据具体需求实现从应用层到内核层的Binder调用监控能力。
Android Hook
具体来说,Hook 技术主要包括以下几个特点: 动态修改: Hook 技术是在程序运行时进行修改,而不是在编译时。这使得它可以灵活地应用于各种场景,而不需要修改程序源码。 透明性: 使用 Hook 技术进行修改是透明的,对于程序的其他部分来说是不可见的。这有利于保持程序的整体一致性和稳定性。 可扩展性: Hook 技术可以用于各种程序功能的扩展和增强,例如系统监控、性能分析、安全检测等。 多样性: Hook 技术可以应用于不同的编程语言和平台,包括 Windows、Linux、macOS 等。它通常利用操作系统或运行时环境提供的钩子机制来实现。
在 Java 中,常见的 Hook 技术包括:
-
使用反射修改现有类的方法实现
-
利用动态代理创建代理对象
-
通过 Java Instrumentation 接口修改类的字节码
-
利用 Java 的 SecurityManager 进行权限控制
在Android系统中,Hook技术通常用于以下几个方面:
1、拦截系统事件:如按键事件、触摸事件等
2、修改系统行为:如改变系统设置、拦截系统调用等
使用 Java 的反射机制可以修改现有类的方法实现。
import java.io.FileInputStream;
import java.lang.reflect.Method;
public class HookFileInputStream {
public static void main(String[] args) throws Exception {
// 获取 FileInputStream 的 read() 方法
Method readMethod = FileInputStream.class.getDeclaredMethod("read");
// 创建一个代理方法,实现自定义逻辑
readMethod.invoke(new FileInputStream("example.txt"), new Object[0]);
}
private static Object proxyRead(Object instance, Method method, Object[] args) throws Throwable {
System.out.println("Before reading file");
Object result = method.invoke(instance, args);
System.out.println("After reading file");
return result;
}
}
3、增强应用功能:如实现应用插件化、动态加载等
三、Hook技术的工作原理
Android Hook技术的核心在于方法拦截。
它通过以下几个步骤实现:
- 获取目标方法或对象: 首先需要确定需要拦截的目标方法或对象。这可以通过反射或动态代理等技术来实现。
- 创建代理类或方法: 创建一个代理类或方法,用于在目标方法或对象被调用时执行自定义的逻辑。
- 替换或修改目标: 将原有的目标方法或对象替换为代理类或方法,使得后续的调用都会指向代理。
- 执行自定义逻辑: 在代理类或方法中执行自定义的逻辑,例如记录日志、修改参数、改变返回值等。
- 可选:恢复原状: 在某些情况下,可能需要在使用完 Hook 技术后将目标方法或对象恢复到原来的状态。
zhuanlan.zhihu.com/p/109157321
Android插件化
Android插件化技术深度解析
1. 插件化的核心价值与实现基础
插件化技术通过将应用功能模块解耦为独立插件,实现按需加载、动态更新和体积优化,尤其适用于大型应用(如电商、社交平台)的复杂需求
1
2
。其核心价值包括:
-
模块化开发:各功能插件可独立开发、测试和更新,降低团队协作成本
2
6
。
-
动态更新能力:无需重新安装APK即可修复BUG或上线新功能,提升用户体验
7
。
-
资源与性能优化:通过插件按需加载,减少主包体积和内存占用,加速启动速度
3
6
。
2. 关键技术组件
2.1 类加载与资源隔离
-
自定义ClassLoader:通过
DexClassLoader动态加载插件中的类,实现宿主与插件的类隔离1
7
。例如,wxdynamicplugin框架通过零反射技术直接操作
ArtMethod,避免传统反射的性能损耗3
。
-
资源管理:需解决资源ID冲突问题,常见方案包括重写AAPT(资源打包工具)或动态分配资源ID
6
7
。
2.2 组件生命周期管理
-
代理机制:对于四大组件(如Activity),传统框架(如DynamicAPK)通过代理类管理生命周期,但需插件继承特定基类
6
。现代框架(如Shadow)则通过全动态化实现,无需修改插件代码
3
。
-
跨进程通信:AIDL或广播机制用于宿主与插件间的数据交互,确保功能解耦
2
4
。
2.3 插件化框架的底层支持
-
Binder机制扩展:通过Hook系统服务(如
ActivityManagerService)或动态修改Binder客户端逻辑,实现插件组件的系统级注册6
7
。
-
资源动态替换:利用
AssetManager.addAssetPath加载插件资源,结合宿主资源形成统一资源池6
7
。
3. 主流框架对比与选型建议
框架
核心技术
优势
局限性
Shadow
零反射、全动态化
无需Hook系统,兼容性好
插件体积较大(3MB+)
3
wxdynamicplugin
零Hack、分段加载
插件体积极小(70KB/模块),启动速度秒级
需自定义编译流程
3
RePlugin
Hook系统服务、多进程支持
高稳定性,支持四大组件动态注册
对系统版本兼容性要求高
5
7
VirtualAPK
动态代理、资源合并
支持插件与宿主资源共享
需预注册插件权限
4
5
选型建议:
-
快速接入:选择RePlugin或VirtualAPK,适合需要快速实现插件化的传统项目
4
5
。
-
极致性能:优先考虑wxdynamicplugin,适用于对启动速度和体积敏感的场景(如音乐类App)
3
。
-
系统兼容性:Shadow框架因零反射特性,更适配Android新版本限制非公开API访问的策略
3
7
。
4. 开发挑战与应对策略
4.1 兼容性与稳定性
-
系统版本适配:不同Android版本对Binder和ClassLoader的实现差异需针对性处理
7
。例如,Android 10+限制Hidden API访问,需通过JNI动态豁免
3
。
-
资源冲突:采用资源ID动态分配或命名空间隔离技术,避免宿主与插件资源冲突
6
7
。
4.2 性能优化
-
冷启动加速:wxdynamicplugin通过插件分段加载(多APK按需加载),减少首次启动耗时
3
。
-
内存管理:及时卸载未使用插件,防止内存泄漏
1
6
。
4.3 动态化与安全
-
热修复集成:结合Tinker或Sophix,实现插件代码与资源的动态替换
7
。
-
代码混淆:对插件核心逻辑加固,防止反编译导致的业务逻辑泄露
6
。
5. 未来趋势
-
轻量化与动态化:进一步压缩插件体积(如wxdynamicplugin的70KB模块),支持插件框架自身动态更新
3
。
-
跨平台融合:结合Flutter或WebAssembly,实现插件多端复用
2
。
-
系统级支持:随着Android对模块化设计的重视,未来可能原生支持插件化标准(如Google Play Dynamic Delivery)
1
7
。
总结
Android插件化技术通过动态加载与资源隔离机制,为大型应用提供了模块化开发与灵活部署的解决方案。开发者需根据业务需求权衡框架特性,例如追求极致性能可选wxdynamicplugin,注重兼容性则采用Shadow或RePlugin。未来,随着动态化技术的演进,插件化将更深度融入系统生态,成为构建高可维护性应用的核心架构
对App的so进行Hook的一种思路
我们知道现在JNI在Android开发中是特别重要的,使用JNI有什么好处呢?
- Preference,C/C++在运行性能上面甩Java几条GAI
- Security,更多的加密解密还是放在Native上。
优点不止这两点,比如在Native里面开辟空间并不受JVM管理,JVM怎么使用native memory。这里不再赘述。
本文提供一种对Android上so库进行Hook的一种思路,不涉及ELF的查看修改,不改动对方的调用方式。 思路就是一招偷梁换柱,用自己的so替换App的so,让对象调用自己的so的时候调用我们自己写的so,我们再调用原来的so,这样就可以获得对方so方法的输入输出。
可以应用在想获取对方App的数据传递格式或者无法破解对方的加解密,但是可以通过hook获取对方的数据格式再调用对方的加解密方法得到自己想要的结果。
Android PLT Hook 技术详解
1. 原理与核心机制
PLT(Procedure Linkage Table)Hook 是一种基于 ELF 文件动态链接机制的 Hook 技术,通过修改 GOT(Global Offset Table) 中的函数地址实现对外部函数调用的拦截
3
4
。其核心逻辑如下:
-
动态链接过程:当 Android 应用调用动态库(如
libc.so)中的函数时,首次调用会通过 PLT 表跳转到动态链接器(Linker)进行符号解析,并将解析后的真实地址写入 GOT 表。后续调用则直接通过 GOT 表跳转至目标函数5
6
。
-
Hook 切入点:PLT Hook 通过替换 GOT 表中的目标函数地址为自定义函数地址,使得所有对该函数的调用均重定向到 Hook 逻辑
1
4
。
2. 实现步骤
-
ELF 文件解析
加载目标 SO 文件,解析其 ELF 结构,定位.dynamic、.rel.plt(重定位表)、.dynsym(动态符号表)等关键节区,获取目标函数在 GOT 表中的偏移地址5
7
。
c// 示例:通过 readelf 工具解析重定位信息 readelf -r libtarget.so | grep 'pthread_create' -
内存基址计算
运行时通过/proc/self/maps获取目标 SO 文件的内存基址,结合 GOT 偏移计算出目标函数在内存中的实际地址6
。
-
函数地址替换
修改目标地址的读写权限(mprotect),将 GOT 表中的原函数地址替换为自定义 Hook 函数地址1
6
。
c// 修改内存权限并替换地址 mprotect((void*)got_page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC); *((void**)got_entry) = custom_pthread_create;
3. 优缺点分析
优点
局限性
稳定性高:仅修改数据段(GOT 表),不破坏代码段,避免触发内存保护机制
3
4
仅支持动态链接函数:无法 Hook 静态链接或模块内函数
3
6
兼容性好:支持 Android 4.1–12 及主流 CPU 架构(armeabi-v7a, arm64-v8a 等)
7
依赖符号表:需目标函数在 ELF 中存在符号信息
3
性能损耗低:首次调用后直接跳转,无额外解析开销
3
无法 Hook 未加载的 SO:需目标库已加载到内存
3
4. 应用场景
-
性能监控
拦截系统调用(如open、pthread_create),统计文件 I/O 耗时、线程创建频率等指标1
6
。
-
安全检测
监控敏感 API(如SSL_write)调用,捕获网络数据明文或加密漏洞4
7
。
-
功能扩展
修改系统行为(如重定向文件读写路径),实现沙箱或虚拟化功能7
。
5. 开源方案对比
框架
特点
适用场景
xHook
轻量级,支持按需 Hook 特定 SO,兼容性良好(IQIYI 开源)
3
线上 APM 监控、轻量化改造
bhook
字节跳动开源,支持 Android 4.1–12,提供线程创建/文件操作等通用 Hook 模板
7
大规模生产环境性能优化
PLT Hook
底层实现灵活,需开发者自行处理 ELF 解析和重定位逻辑
5
定制化需求或学术研究
6. 实践建议
-
选择 Hook 点:优先选择高频、低风险函数(如
malloc/free),避免 Hook 关键路径函数导致性能劣化3
6
。
-
线程安全:Hook 过程中需加锁或暂停目标线程,防止并发修改 GOT 表引发崩溃
1
。
-
兼容性测试:针对不同 Android 版本和厂商 ROM 验证 Hook 稳定性(如华为 EMUI 对 SO 加载的特殊处理)
7
。
总结
PLT Hook 凭借其稳定性和低侵入性,成为 Android Native 层监控与优化的主流方案。开发者可根据需求选择成熟的开源框架(如 bhook)或定制底层实现,结合性能、安全、兼容性等多维度评估,构建高效的动态化能力
3
7
。