背景

最近收到联想市场关于启动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去指定

<application ...>
    <meta-data
            android:name="io.flutter.embedding.android.OldGenHeapSize"
            android:value="1024" />
</application>

然后我也问了,给出这个方案的老哥,这个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<TransformInput> inputs = transformInvocation.inputs;
        TransformOutputProvider outputProvider = transformInvocation.outputProvider;
        if (outputProvider != null) {
            outputProvider.deleteAll()
        }
        // 遍历每个input数据
        System.out.println("-----start input")
        for (TransformInput input : inputs) {
            Collection<DirectoryInput> 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的别的版本的可以根据源码微微调整

还有flutter issue里虽然说了新版本已经修改了,但我升级到flutter 2.5.3发现依然存在,基于此做了代码插桩修复。而且对于flutter版本的升级也需要慎重。得做足测试和灰度才能去升级的,避免引起新的问题。

more

对于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, "<init>", "()V", null, null);
            methodVisitor.visitCode();
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()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, "<init>", "()V", null, null);
            methodVisitor.visitCode();
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()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修改字节码也不失为一种解决方案了。对代码入侵其实也不算大

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐