前言
一般产品或项目前期都是以快速实现,上线的方式来完成,在生产环境中再开始进行优化,而Android的APP优化,比较重点的还是内存优化,因为每个APP都分配的最大内存,像内存泄露,内存抖动等慢慢都会让APP出来OOM崩溃的情况,最近也是一直在学习和研究内存优化这块,也是在实践中记录笔记。
JVMTI
JVMTI 本质上是在JVM内部的许多事件进行了埋点,通过这些埋点可以给外部提供当前上下文的一些信息。
从 Android 8.0 开始,Android ART已经加入了JVMTI的相关功能。目录位于art/runtime/openjdkjvmti下,从Android.bp可以看到,编译会生成libopenjdkjvmtid.so、libopenjdkjvmti.so文件,其中核心文件是jvmti.h文件,里面定义了一些核心方法和结构体。本地实现时,需要引入该文件来实现对应的Capabilities。
看到.so文件,很明显就是想使用JVMTI,就要用JNI的方式去进行调用了,接下来我们直接从代码上实现。
代码实现
因为要使用JNI,所以项目要创建一个Native C 的项目,完整的Demo源码会在文章最后放出来。
项目目录
01创建Monitor监听类
监听类里面主要就是初始化JVMTI,包括启动和释放,另外加入一个过滤的函数,使用JVMTI监听时,会将所有的对象和方法都列出来,做为线上监听,我们需要写入本地文件里到时可以查看,如果所有的方法都写入,文件会特别大,所以加了一个函数用于只写入我们想要得到的信息。
attachAgent开启JVMTI
代码attachAgent函数是初始化JVMTI的使用,在Android9.0中已将API添加到framework/base/core/java/android/os/Debug.java中,可以直接调用,而Android9.0以下的,需要通过反射的方法进行调用。
JNI方法
定义了三个JNI的方法,用于初始化,释放和过滤要存文件的内容,具体的实现在native-lib.cpp中。
Moniter代码
代码语言:javascript复制package pers.vaccae.memorymonitor
import android.content.Context
import android.os.Build
import android.os.Debug
import android.util.Log
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths
import java.text.SimpleDateFormat
import java.util.*
/**
* 作者:Vaccae
* 邮箱:3657447@qq.com
* 创建时间:15:13
* 功能模块说明:
*/
object Monitor {
private const val LIB_NAME = "libmemorymonitor.so"
fun init(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//查找SO的路径
val libDir: File = File(context.filesDir, "lib")
if (!libDir.exists()) {
libDir.mkdirs()
}
//判断So库是否存在,不存在复制过来
val libSo: File = File(libDir, LIB_NAME)
if (libSo.exists()) libSo.delete()
val findLibrary =
ClassLoader::class.java.getDeclaredMethod("findLibrary", String::class.java)
val libFilePath = findLibrary.invoke(context.classLoader, "memorymonitor") as String
Log.i("jvmti", "so Path:$libFilePath")
Files.copy(
Paths.get(File(libFilePath).absolutePath), Paths.get(
libSo.absolutePath
)
)
//加载SO库
val agentPath = libSo.absolutePath
System.load(agentPath)
//agent连接到JVMTI
attachAgent(agentPath, context.classLoader);
//开启JVMTI事件监听
val logDir = File(context.filesDir, "log")
if (!logDir.exists()) logDir.mkdir()
//获取当前时间
val formatter = SimpleDateFormat("yyyyMMddHHmmss")
val curDate= formatter.format(Date(System.currentTimeMillis()))
val path = "${logDir.absolutePath}/${curDate}.log"
attachInit(path)
} else {
Log.i("jvmti", "系统版本无法全用JVMTI")
}
}
//agent连接到JVMTI
private fun attachAgent(agentPath: String, classLoader: ClassLoader) {
//Android 9.0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
Debug.attachJvmtiAgent(agentPath, null, classLoader)
} else {
//android 9.0以下版本使用反射方式加载
val vmDebugClazz = Class.forName("dalvik.system.VMDebug")
val attachAgentMethod = vmDebugClazz.getMethod("attachAgent", String::class.java)
attachAgentMethod.isAccessible = true
attachAgentMethod.invoke(null, agentPath)
}
}
fun release() {
attachRelease()
}
fun writeFilters(pkgname:String){
Log.i("jvmti",pkgname)
attachWFilters(pkgname)
}
//region JNI函数
//开启JVMTI事件监听
private external fun attachInit(path: String)
private external fun attachRelease()
private external fun attachWFilters(packagename: String)
//endregion
}
02拷贝jvmti.h文件
Android的安装目录下有JDK,如果自己安装的JDK,也可以在安装的JDK目录的include下看到jvmti.h的头文件,将这个jvmti.h的头文件拷贝到程序目录cpp下。
当attacchAgent开启监听后,会执行一个回调函数,可以在jvmti.h中看到,我们在C 文件中写这个回调方法的实现用于加载要监听的东西的参数配置
像监听的回调方法,也是在这个头文件中找到,这次我们就监听对象的创建和函数的调用两个方法,如下:
03C nativ-lib中实现回调
在jvmti.h中拷过来后可以看到相关的回调函数了,在native-lib.cpp中主要就是写三个回调方法的实现。
Agent_OnAttach(初始化回调)
objectAlloc(对象创建时的回调)
methodEntry(函数进入时的回调)
JNI attachInit实现初始化的函数
native-lib.cpp完整代码
代码语言:javascript复制#include <jni.h>
#include <string>
#include <android/log.h>
#include <chrono>
#include "jvmti.h"
#include "MemoryFile.h"
#define LOG_TAG "jvmti"
#define ALOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
#define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
jvmtiEnv *mJvmtiEnv;
MemoryFile *memoryFile;
jlong tag = 0;
std::string mPackageName;
//查找过滤
jboolean findFilter(const char *name) {
std::string tmpstr = name;
int idx;
//先判断甩没有Error,有Error直接输出
idx = tmpstr.find(mPackageName);
if (idx == std::string::npos) {
idx = tmpstr.find("OutOfMemoryError");
if (idx == std::string::npos)//不存在。
{
return JNI_FALSE;
} else {
return JNI_TRUE;
}
} else {
return JNI_TRUE;
}
}
// 获取当时系统时间
std::string GetCurrentSystemTime() {
//auto t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
auto now = std::chrono::system_clock::now();
//通过不同精度获取相差的毫秒数
uint64_t dis_millseconds =
std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count()
- std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count() * 1000;
time_t tt = std::chrono::system_clock::to_time_t(now);
struct tm *ptm = localtime(&tt);
char date[60] = {0};
sprintf(date, "%d-d-d d:d:d.d",
(int) ptm->tm_year 1900, (int) ptm->tm_mon 1, (int) ptm->tm_mday,
(int) ptm->tm_hour, (int) ptm->tm_min, (int) ptm->tm_sec, (int) dis_millseconds);
return move(std::string(date));
}
jvmtiEnv *CreateJvmtiEnv(JavaVM *vm) {
jvmtiEnv *jvmti_env;
jint result = vm->GetEnv((void **) &jvmti_env, JVMTI_VERSION_1_2);
if (result != JNI_OK) {
ALOGI("CreateJvmtiEnv is NULL");
return nullptr;
}
return jvmti_env;
}
//调用System.Load()后会回调该方法
extern "C"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
ALOGI("JNI_OnLoad");
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
ALOGI("JNI_OnLoad Finish");
return JNI_VERSION_1_6;
}
void JNICALL objectAlloc(jvmtiEnv *jvmti_env, JNIEnv *jni_env, jthread thread,
jobject object, jclass object_klass, jlong size) {
//给对象打tag,后续在objectFree()内可以通过该tag来判断是否成对出现释放
tag = 1;
jvmti_env->SetTag(object, tag);
//获取线程信息
jvmtiThreadInfo threadInfo;
jvmti_env->GetThreadInfo(thread, &threadInfo);
//获得 创建的对象的类签名
char *classSignature;
jvmti_env->GetClassSignature(object_klass, &classSignature, nullptr);
if (mPackageName.empty() || findFilter(classSignature)) {
//写入日志文件
char str[500];
char *format = "%s: object alloc {Thread:%s Class:%s Size:%lld Tag:%lld} rn";
//ALOGI(format, GetCurrentSystemTime().c_str(),threadInfo.name, classSignature, size, tag);
sprintf(str, format, GetCurrentSystemTime().c_str(), threadInfo.name, classSignature, size,
tag);
memoryFile->write(str, sizeof(char) * strlen(str));
}
jvmti_env->Deallocate((unsigned char *) classSignature);
}
void JNICALL methodEntry(jvmtiEnv *jvmti_env, JNIEnv *jni_env, jthread thread, jmethodID method) {
jclass clazz;
char *signature;
char *methodName;
//获得方法对应的类
jvmti_env->GetMethodDeclaringClass(method, &clazz);
//获得类的签名
jvmti_env->GetClassSignature(clazz, &signature, nullptr);
//获得方法名字
jvmti_env->GetMethodName(method, &methodName, nullptr, nullptr);
if (mPackageName.empty() || findFilter(signature)) {
//写日志文件
char str[500];
char *format = "%s: methodEntry {%s %s} rn";
//ALOGI(format, GetCurrentSystemTime().c_str(), signature, methodName);
sprintf(str, format, GetCurrentSystemTime().c_str(), signature, methodName);
memoryFile->write(str, sizeof(char) * strlen(str));
}
jvmti_env->Deallocate((unsigned char *) methodName);
jvmti_env->Deallocate((unsigned char *) signature);
}
extern "C"
JNIEXPORT void JNICALL
Java_pers_vaccae_memorymonitor_Monitor_attachInit(JNIEnv *env, jobject thiz, jstring path) {
ALOGI("attachInit");
const char *_path = env->GetStringUTFChars(path, NULL);
ALOGI("mPackageName:%s", mPackageName.c_str());
memoryFile = new MemoryFile(_path);
//开启JVMTI事件监听
jvmtiEventCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.VMObjectAlloc = &objectAlloc;
callbacks.MethodEntry = &methodEntry;
ALOGI("SetEventCallbacks");
//设置回调函数
int error = mJvmtiEnv->SetEventCallbacks(&callbacks, sizeof(callbacks));
ALOGI("返回码:%dn", error);
//开启监听
mJvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_OBJECT_ALLOC, nullptr);
mJvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, nullptr);
env->ReleaseStringUTFChars(path, _path);
ALOGI("attachInit Finished");
}
//初始化工作
extern "C"
JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM *vm, char *options, void *reserved) {
int error;
//准备JVMTI环境
mJvmtiEnv = CreateJvmtiEnv(vm);
//开启JVMTI的能力
jvmtiCapabilities caps;
mJvmtiEnv->GetPotentialCapabilities(&caps);
mJvmtiEnv->AddCapabilities(&caps);
ALOGI("Agent_OnAttach Finish");
return JNI_OK;
}
extern "C"
JNIEXPORT void JNICALL
Java_pers_vaccae_memorymonitor_Monitor_attachRelease(JNIEnv *env, jobject thiz) {
delete memoryFile;
//关闭监听
mJvmtiEnv->SetEventNotificationMode(JVMTI_DISABLE, JVMTI_EVENT_VM_OBJECT_ALLOC, NULL);
mJvmtiEnv->SetEventNotificationMode(JVMTI_DISABLE, JVMTI_EVENT_METHOD_ENTRY, NULL);
}
extern "C"
JNIEXPORT void JNICALL
Java_pers_vaccae_memorymonitor_Monitor_attachWFilters(JNIEnv *env, jobject thiz,
jstring packagename) {
const char *_packagename = env->GetStringUTFChars(packagename, NULL);
mPackageName = std::string(_packagename);
env->ReleaseStringUTFChars(packagename, _packagename);
}
04日志写入文件MemoryFile
建一个MemoryFile的C 类,通过这个类实现消息往MemoryFIle中写入。
MemoryFile.h
代码语言:javascript复制//
// Created by 36574 on 2022-03-25.
//
#ifndef MEMORYMONITOR_MEMORYFILE_H
#define MEMORYMONITOR_MEMORYFILE_H
class MemoryFile {
private:
const char* m_path;
int m_fd;
int32_t m_size;
int8_t *m_ptr;
int m_actualSize;
void resize(int32_t needSize);
public:
MemoryFile(const char *path);
~MemoryFile();
void write(char *data, int dataLen);
};
#endif //MEMORYMONITOR_MEMORYFILE_H
MemoryFile.cpp
代码语言:javascript复制//
// Created by 36574 on 2022-03-25.
//
#include <cstdint>
#include <unistd.h>
#include <cstring>
#include <fcntl.h>
#include <sys/mman.h>
#include <mutex>
#include "MemoryFile.h"
std::mutex mtx;
//系统给我们提供真正的内存时,用页为单位提供
//内存分页大小 一分页的大小
int32_t DEFAULT_FILE_SIZE = getpagesize();
MemoryFile::MemoryFile(const char *path) {
m_path = path;
m_fd = open(m_path, O_RDWR | O_CREAT, S_IRWXU);
m_size = DEFAULT_FILE_SIZE;
//将文件设置为m_size大小
ftruncate(m_fd, m_size);
//mmap内存映射
m_ptr = static_cast<int8_t *>(mmap(0, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0));
//初始化m_actualSize为0
m_actualSize = 0;
}
MemoryFile::~MemoryFile() {
munmap(m_ptr, m_size);
close(m_fd);
}
void MemoryFile::write(char *data, int dataLen) {
mtx.lock();
if(m_actualSize dataLen >= m_size){
resize(m_actualSize dataLen);
}
//将data的datalen长度的数据 拷贝到 m_ptr m_actualSize;
//操作内存,通过内存映射就写入文件了
memcpy(m_ptr m_actualSize, data, dataLen);
//重新设置最初位置
m_actualSize = dataLen;
mtx.unlock();
}
void MemoryFile::resize(int32_t needSize) {
int32_t oldSize = m_size;
do{
m_size *=2;
} while (m_size<needSize);
//设置文件大小
ftruncate(m_fd, m_size);
//解除映射
munmap(m_ptr, oldSize);
//重新进行mmap内存映射
m_ptr = static_cast<int8_t *>(mmap(0,m_size,PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0));
}
05CMakeList中加入MemoryFile.cpp
加入了文件写入的类,所以要在CMakeList中加入进来这个cpp
06写一个OOM的操作实现效果
定义一个Byte数组,直接就是1G,肯定会OOM
在MainActivity中初始化这个类
自己定义的Application中OnCreate直接初始化JVMTI监听,并且只留下含有vaccae的信息和错误信息。
实现效果
设备的data/data/包名/files下面现在是空的,我们直接运行程序
可以看到,一运行就直接OutOfMemoryError了
重新刷新data/data/包名/files/log下有一个当前时间的log文件,把它导出到电脑上
打开log文件后可以看到,OutOfMemoryError处上方,执行的是ByteTest中的init方法,也就是我们代码中MainActivity的OnCreate是ByteTest()。这样就可以定位的错误的位置了。
重点
上面的真机用的是android9.0以后的,所以没有问题,代码中也写了8.0用的反射方法,我也专门创建了android8.1的虚拟机,发现上面的方式并不能用,下一篇就专门针对android8.1怎么实现讲解。
源码地址
https://github.com/Vaccae/AndroidJVMTIDemo.git
完