最近收到联想市场关于启动app崩溃问题的报告,进过排查发现是由于flutter导致的。报错如下
"E/DartVM (13711): Exhausted heap space, trying to allocate 8 bytes" is printed by the Dart VM when it fails to allocate an object even after atempting a Full Garbage Collection. This will result in an OOM exception being thrown from Dart.
看到这个意思是说dartVM想申请8byte的内存却没能申请成功,但这也不科学啊。我才打开app,都没加载过多的flutter页面。
解决方案而且联想应用市场也说只有18g内存的手机有这个问题。然后我就去github的flutter中的issue搜索了下,果不其然已经有人遇到这个问题了。
https://github.com/flutter/flutter/issues/86855
有兴趣的可以看看,里面也已经说了出现的原因。但给出的解决方案其实挺暴力的,因为这个old_gen_heap_size可以通过manifest文件的metaData去指定
然后我也问了,给出这个方案的老哥,这个1024也是他们的临时方案。
我们看看原始代码,分析给出一个合理的大小。
ApplicationInfo applicationInfo =
applicationContext
.getPackageManager()
.getApplicationInfo(
applicationContext.getPackageName(), PackageManager.GET_META_DATA);
Bundle metaData = applicationInfo.metaData;
int oldGenHeapSizeMegaBytes =
metaData != null ? metaData.getInt(OLD_GEN_HEAP_SIZE_META_DATA_KEY) : 0;
if (oldGenHeapSizeMegaBytes == 0) {
// default to half of total memory.
ActivityManager activityManager =
(ActivityManager) applicationContext.getSystemService(Context.ACTIVITY_SERVICE);
ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memInfo);
oldGenHeapSizeMegaBytes = (int) (memInfo.totalMem / 1e6 / 2);
}
可以看到,这个oldGenHeapSize其实是通过计算给出的。
方案一那我怎么去修改这个值呢
一开始我想着动态去修改过manifest中的这个值,通过java代码的方式
ApplicationInfo appInfo = null;
try {
appInfo = SoftApplication.getContext().getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
appInfo.metaData.putString("io.flutter.embedding.android.OldGenHeapSize","1024");
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
这是get 拿到bunder去修改这个值。
正当我信心满满去验证时才发现我设置的值没生效。经过查阅资料发现这样获取的ApplicationInfo
其实是一个副本。这就很难受
然后我尝试换方案,之前我用反射加动态代理解决过很多棘手问题,然后我想着试试反射?
然后我找到代码关键尝试去hook
找到相关类FlutterLoader
的关键方法ensureInitializationComplete
看了下,没有能下手的地方。
shellArgs.add("--old-gen-heap-size=" + oldGenHeapSizeMegaBytes);
if (metaData != null && metaData.getBoolean(ENABLE_SKPARAGRAPH_META_DATA_KEY)) {
shellArgs.add("--enable-skparagraph");
}
long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis;
flutterJNI.init(
applicationContext,
shellArgs.toArray(new String[0]),
kernelPath,
result.appStoragePath,
result.engineCachesPath,
initTimeMillis);
initialized = true;
这段代码我也无能为力。
而且这个FutterJNI
这不是个接口,无法动态代理去hook,此路也不通了。
这下真的完犊子了。想着算了 不完美就不完美吧用 在manifest的方式解决吧。
这件事放了2天,我还是放不下,后来突然想起来 我可以在编译class阶段修改字节码修改下代码吗。
asm就有这个能力,而且之前也尝试写过类似的小功能。来说干就干吧
首先是编写gradle插件,在编译代码期间找到FutterJNI
这个类,然后在init方法的头部找到args参数,修改字符串包含**–old-gen-heap-size=**这项的值
flutterJNI.init(
applicationContext,
shellArgs.toArray(new String[0]),
kernelPath,
result.appStoragePath,
result.engineCachesPath,
initTimeMillis);
下面是asm的重要部分代码,具体代码时间我已经上传github了
FixFlutter220
遍历jar文件和file文件
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
Collection inputs = transformInvocation.inputs;
TransformOutputProvider outputProvider = transformInvocation.outputProvider;
if (outputProvider != null) {
outputProvider.deleteAll()
}
// 遍历每个input数据
System.out.println("-----start input")
for (TransformInput input : inputs) {
Collection directoryInputs = input.directoryInputs;
for (DirectoryInput directoryInput : directoryInputs) {
handleDirectoryInput(directoryInput, outputProvider)
}
for (JarInput jarInput : input.jarInputs) {
handleJarInputs(jarInput, outputProvider)
}
}
}
/**
* 处理文件目录下的class文件
*/
static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
//是否是目录
if (directoryInput.file.isDirectory()) {
//列出目录所有文件(包含子文件夹,子文件夹内文件)
directoryInput.file.eachFileRecurse { File file ->
def name = file.name
// 检测这个类是不是我需要处理的类
if (checkClassFile(name)) {
println '----------- deal with "class" file <' + name + '> -----------'
ClassReader classReader = new ClassReader(file.bytes)
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new FixFlutterClassVisitor(Opcodes.ASM4, classWriter)
classReader.accept(cv, ClassReader.EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
FileOutputStream fos = new FileOutputStream(
file.parentFile.absolutePath + File.separator + name)
fos.write(code)
fos.close()
}
}
}
//处理完输入文件之后,要把输出给下一个任务
File dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}
/**
* 处理Jar中的class文件
*/
static void handleJarInputs(JarInput jarInput, TransformOutputProvider outputProvider) {
if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
//重名名输出文件,因为可能同名,会覆盖
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
JarFile jarFile = new JarFile(jarInput.file)
Enumeration enumeration = jarFile.entries()
File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
//避免上次的缓存被重复插入
if (tmpFile.exists()) {
tmpFile.delete()
}
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
//用于保存
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement()
String entryName = jarEntry.getName()
ZipEntry zipEntry = new ZipEntry(entryName)
InputStream inputStream = jarFile.getInputStream(jarEntry)
//插桩class
if (checkClassFile(entryName)) {
//class文件处理
println '----------- deal with "jar" class file <' + entryName + '> -----------'
jarOutputStream.putNextEntry(zipEntry)
ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new FixFlutterClassVisitor(Opcodes.ASM4, classWriter)
classReader.accept(cv, ClassReader.EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
jarOutputStream.write(code)
} else {
jarOutputStream.putNextEntry(zipEntry)
jarOutputStream.write(IOUtils.toByteArray(inputStream))
}
jarOutputStream.closeEntry()
}
//结束
jarOutputStream.close()
jarFile.close()
def dest = outputProvider.getContentLocation(jarName + md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(tmpFile, dest)
tmpFile.delete()
}
}
找到处理类后,对class进行访问,找到init方法
FixFlutterClassVisitor
/**
* File description.
* 针对FlutterJNI这个类 然后找到init方法 然后去修改它
*
* @author lihongjun
* @date 11/8/21
*/
public class FixFlutterClassVisitor extends ClassVisitor implements Opcodes {
public FixFlutterClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
/**
* 每个方法编译时都会走到这
* @param access
* @param name
* @param descriptor
* @param signature
* @param exceptions
* @return
*/
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
// 如果是init方法
if (name.contains("init")) {
System.out.println("----init");
return new FixFlutterMethodVisitor(name, this.api, mv, descriptor, access);
}
return mv;
}
}
找到方法后对方法入参数进行修改
/**
* File description.
* 找到init方法修改入参值
*
* @author lihongjun
* @date 11/8/21
*/
public class FixFlutterMethodVisitor extends MethodVisitor {
private String description;
int accessFlag;
public FixFlutterMethodVisitor(String name,int api,MethodVisitor methodVisitor,String description,int accessFlag) {
super(api,methodVisitor);
this.description = description;
this.accessFlag = accessFlag;
}
/**
* 方法入口
*/
@Override
public void visitCode() {
Type[] argTypes = Type.getArgumentTypes(description);
if (null != argTypes) {
for (Type type : argTypes) {
System.out.println("arg type:" + type.getClassName());
// flutter初始化入参是String[]是 启动arg配置
if (type.getClassName().equals("java.lang.String[]")) {
System.out.println("insert data start");
visitVarInsn(Opcodes.ALOAD, 1);
visitVarInsn(Opcodes.ALOAD, 2);
// 此处是我的工具类的路径
visitMethodInsn(Opcodes.INVOKESTATIC, "cn/lhj/module/base/utils/OsUtils", "fixFlutter32B18GCrash", "(Landroid/content/Context;[Ljava/lang/String;)V", false);
System.out.println("insert data end");
break;
}
}
}
super.visitCode();
}
}
最后插入工具类去执行修复逻辑
/**
* File description.
* 系统信息获取
* @author lihongjun
* @date 11/3/21
*/
public class OsUtils {
/**
* 获取app运行内存
* @param context
* @return
*/
public static long getAppRUnMemory(Context context) {
if (context == null) {
return 0;
}
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); //系统内存信息
if (am == null) {
return 0;
}
ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo();
am.getMemoryInfo(memInfo);
return memInfo.totalMem;
}
/**
* 修复18G运行内存手机在32位app上崩溃的问题
*/
public static void fixFlutter32B18GCrash(Context context,String arg[]) {
Log.e("lhj","fixFlutter32B18GCrash");
if (arg == null) {
return;
}
try {
long runMemory = getAppRUnMemory(context);
Log.e("lhj",runMemory + "");
if (runMemory > 16L * 1024L * 1024L * 1000L) {
Log.i("lhj","大于16G运存");
int length = arg.length;
for (int i = 0 ; i < length; i ++) {
if (arg[i] != null && arg[i].contains("--old-gen-heap-size=")) {
arg[i] = "--old-gen-heap-size=1024";
Log.e("lhj","findData and fix");
break;
}
}
} else {
Log.i("lhj","小于16G运存 不做任何修改");
}
} catch (Exception e) {
e.printStackTrace();
Log.e("lhj",e.getMessage());
}
}
}
为什么要使用这种方式去做,一来,对小于18g内存的手机不做修改,减少影响范围,再者对于1024这个值可以是动态计算出来的。可以根据不同状况自己定义一个合适值。插件编写完成后只需要在我们工程引入这个插件就行了。
使用方式在项目根目录引入写好的插件
buildscript {
repositories {
google()
jcenter()
maven {
url uri('/Users/lihongjun/StudioProjects/FlutterFix/repo')
}// 插件仓库地址
}
dependencies {
classpath "com.android.tools.build:gradle:4.1.2"
classpath 'cn.lhj.flutter_fix.plugin:fix_flutter_220:1.0.38'//插件版本
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Android项目build文件引入插件
plugins {
id 'com.android.application'
}
apply plugin: 'fixflutter220' // 这里是我们编写的gradle 插件
android {
compileSdkVersion 30
buildToolsVersion "30.0.2"
defaultConfig {
applicationId "cn.lhj.flutterfix"
minSdkVersion 23
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
还有把上面的OsUtils
放在指定目录下,它的路径(包裹包名)和插桩代码有关系,如下面的插桩代码,在FixFlutterMethodVisitor
中
visitMethodInsn(Opcodes.INVOKESTATIC, "cn/lhj/module/base/utils/OsUtils", "fixFlutter32B18GCrash", "(Landroid/content/Context;[Ljava/lang/String;)V", false);
github地址这里是插件和demo代码
当然这个asm代码插桩是基于flutter2.2.0的别的版本的可以根据源码微微调整
more还有flutter issue里虽然说了新版本已经修改了,但我升级到flutter 2.5.3发现依然存在,基于此做了代码插桩修复。而且对于flutter版本的升级也需要慎重。得做足测试和灰度才能去升级的,避免引起新的问题。
对于asm插桩听起来和玄乎 其实也不算难,下面简单介绍下。首先我们装个android studio插件 asm bytecode outline
,那我们要修改字节码的的FlutterJNI
来说,下面是源代码
public class FlutterJNI {
public void init(@NonNull Context context,
@NonNull String[] args,
@Nullable String bundlePath,
@NonNull String appStoragePath,
@NonNull String engineCachesPath,
long initTimeMillis) {
Log.e("lhj",args[0]);
}
public void test() {
}
}
查看它的字节码
public class FlutterJNIDump implements Opcodes {
public static byte[] dump() throws Exception {
ClassWriter classWriter = new ClassWriter(0);
FieldVisitor fieldVisitor;
MethodVisitor methodVisitor;
AnnotationVisitor annotationVisitor0;
classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "cn/lhj/flutterfix/FlutterJNI", null, "java/lang/Object", null);
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "" , "()V", null, null);
methodVisitor.visitCode();
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "" , "()V", false);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();
}
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "init", "(Landroid/content/Context;[Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;J)V", null, null);
methodVisitor.visitAnnotableParameterCount(6, false);
{
annotationVisitor0 = methodVisitor.visitParameterAnnotation(0, "Landroidx/annotation/NonNull;", false);
annotationVisitor0.visitEnd();
}
{
annotationVisitor0 = methodVisitor.visitParameterAnnotation(1, "Landroidx/annotation/NonNull;", false);
annotationVisitor0.visitEnd();
}
{
annotationVisitor0 = methodVisitor.visitParameterAnnotation(2, "Landroidx/annotation/Nullable;", false);
annotationVisitor0.visitEnd();
}
{
annotationVisitor0 = methodVisitor.visitParameterAnnotation(3, "Landroidx/annotation/NonNull;", false);
annotationVisitor0.visitEnd();
}
{
annotationVisitor0 = methodVisitor.visitParameterAnnotation(4, "Landroidx/annotation/NonNull;", false);
annotationVisitor0.visitEnd();
}
methodVisitor.visitCode();
methodVisitor.visitLdcInsn("lhj");
methodVisitor.visitVarInsn(ALOAD, 2);
methodVisitor.visitInsn(ICONST_0);
methodVisitor.visitInsn(AALOAD);
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
methodVisitor.visitInsn(POP);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(3, 8);
methodVisitor.visitEnd();
}
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "test", "()V", null, null);
methodVisitor.visitCode();
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(0, 1);
methodVisitor.visitEnd();
}
classWriter.visitEnd();
return classWriter.toByteArray();
}
}
关键点的代码在这
methodVisitor.visitCode();
methodVisitor.visitLdcInsn("lhj");
methodVisitor.visitVarInsn(ALOAD, 2);
methodVisitor.visitInsn(ICONST_0);
methodVisitor.visitInsn(AALOAD);
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
methodVisitor.visitInsn(POP);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(3, 8);
methodVisitor.visitEnd();
我们要做的其实在Log.e("lhj","arg[0")
的上面插入我们的修复代码,那我们尝试加下修复代码看看它是什么样子的。
原代码
public class FlutterJNI {
public void init(@NonNull Context context,
@NonNull String[] args,
@Nullable String bundlePath,
@NonNull String appStoragePath,
@NonNull String engineCachesPath,
long initTimeMillis) {
OsUtils.fixFlutter32B18GCrash(context,args); // 我们要插入的代码
Log.e("lhj",args[0]);
}
public void test() {
}
}
byeCode
public class FlutterJNIDump implements Opcodes {
public static byte[] dump() throws Exception {
ClassWriter classWriter = new ClassWriter(0);
FieldVisitor fieldVisitor;
MethodVisitor methodVisitor;
AnnotationVisitor annotationVisitor0;
classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "cn/lhj/flutterfix/FlutterJNI", null, "java/lang/Object", null);
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "" , "()V", null, null);
methodVisitor.visitCode();
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "" , "()V", false);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();
}
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "init", "(Landroid/content/Context;[Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;J)V", null, null);
methodVisitor.visitAnnotableParameterCount(6, false);
{
annotationVisitor0 = methodVisitor.visitParameterAnnotation(0, "Landroidx/annotation/NonNull;", false);
annotationVisitor0.visitEnd();
}
{
annotationVisitor0 = methodVisitor.visitParameterAnnotation(1, "Landroidx/annotation/NonNull;", false);
annotationVisitor0.visitEnd();
}
{
annotationVisitor0 = methodVisitor.visitParameterAnnotation(2, "Landroidx/annotation/Nullable;", false);
annotationVisitor0.visitEnd();
}
{
annotationVisitor0 = methodVisitor.visitParameterAnnotation(3, "Landroidx/annotation/NonNull;", false);
annotationVisitor0.visitEnd();
}
{
annotationVisitor0 = methodVisitor.visitParameterAnnotation(4, "Landroidx/annotation/NonNull;", false);
annotationVisitor0.visitEnd();
}
methodVisitor.visitCode();
// 关键代码开始
methodVisitor.visitVarInsn(ALOAD, 1);
methodVisitor.visitVarInsn(ALOAD, 2);
methodVisitor.visitMethodInsn(INVOKESTATIC, "cn/lhj/flutterfix/OsUtils", "fixFlutter32B18GCrash", "(Landroid/content/Context;[Ljava/lang/String;)V", false);
// 关键代码结束
methodVisitor.visitLdcInsn("lhj");
methodVisitor.visitVarInsn(ALOAD, 2);
methodVisitor.visitInsn(ICONST_0);
methodVisitor.visitInsn(AALOAD);
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
methodVisitor.visitInsn(POP);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(3, 8);
methodVisitor.visitEnd();
}
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "test", "()V", null, null);
methodVisitor.visitCode();
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(0, 1);
methodVisitor.visitEnd();
}
classWriter.visitEnd();
return classWriter.toByteArray();
}
}
看看代码里的注视,你是不是发现挺简单了,这一切还是要依托于咋们的 asm bytecode outline
的功劳。以后遇到第三方sdk有问题,他们还没及时更新时,而恰好你有解决方案是,通过asm修改字节码也不失为一种解决方案了。对代码入侵其实也不算大
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)