当前位置:   article > 正文

Android 大图检测插件的落地_android 大图 监控

android 大图 监控

作者:layz4android

在实际的项目开发中,引入图片的方式基本可以分为两种:本地图片和云端图片,对于云端图片来说,可以动态地配置图片的大小,如果服务端的伙伴下发的图片很大导致程序异常,那么可以随时修改端上无法发版修复。但是因为本地引入的图片,e.g. 背景图,位图 过大导致程序出现异常,则必须要紧急发版修复线上用户问题。

其实在开发过程中是可以规避这个问题的,只不过是少了一个check的过程,而且大部分因为开发过程中不规范导致问题的发生,UI切图的不规范,或者开发者没有在意尺寸大小而随意引入到工程中。因为开发中可能频繁地installDebug,一些内存问题并不会发现,而到了线上之后,在用户场景中可能就会出现。

我们都知道,大图是导致OOM的真凶之一,对大图做工程级的check,就能避免类似问题的出现。

1 图片加载到内存的大小

对于这个问题,伙伴们应该也比较熟悉,看下面这张图

这张图的总大小只有73kb,但是这个是一个具有欺骗性的数据,这个大小只会决定传输速度,而不是在内存中就占用73kb,那么怎么计算这张图片加载到内存中有多大呢?其实是有一个计算公式的:分辨率 * 每个像素点大小

所以这张图片加载到内存中占用:512 x 432 x 4(从图中看,一个像素32bit = 4Byte) = 884736Byte

也就是说,这张图片加载到内存中就需要占用885k的内存空间,而且系统对于res目录下的资源加载,如果是不同的drawable目录,例如drawable-xhdpi、drawable-xxhdpi,都是先进行分辨率的转换,再加载到内存。 而且我们在显示这张图片的时候,设置ImageView的大小为40dpx40dp,显然这张图片是过大了。

2 大图检测插件的落地

前面我们提到,如果出现这种大图,加载到内存中其实会浪费一些内存资源,那么我们有什么手段去做做优化或者避免这种情况发生呢?

2.1 大图加载的优化策略

其实图片在加载到内存中时,就是会走系统的BitmapFactory工厂类,在BitmapFactory.Option中也是提供了对应的方法;

(1)在没有加载到内存之前,获取图片的宽高,进行等比缩放;
(2)通过inBitmap实现内存复用。

除此之外,我们还会用到一些图片加载框架,像Glide,它可以在加载的图片的时候,根据容器的大小按需加载,但是也是存在局限,就是无法处理xml文件中的background属性或者src属性,也就本地的图片无法做到兼容处理,所以使用Glide依然无法做到工程级别的大图兼容问题。

所以本文介绍的大图检测插件,就是解决Glide无法兼容本地图片加载的问题,对于开发者引入的大图可以在运行的时候做检测,并提示开发者存在不合理的大图,需要进行修改。

2.2 大图检测的思想

其实我们在加载本地图片的时候,大部分都是通过ImageView来进行展示,即便是自定义View,也都是通过继承自ImageView或者AppCompatImageView来进行逻辑处理,因此我们需要关注下ImageView展示图片的逻辑。

public void setImageDrawable(@Nullable Drawable drawable) {
    if (mDrawable != drawable) {
        mResource = 0;
        mUri = null;

        final int oldWidth = mDrawableWidth;
        final int oldHeight = mDrawableHeight;

        updateDrawable(drawable);

        if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
            requestLayout();
        }
        invalidate();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

@android.view.RemotableViewMethod(asyncImpl="setImageIconAsync")
public void setImageIcon(@Nullable Icon icon) {
    setImageDrawable(icon == null ? null : icon.loadDrawable(mContext));
}
  • 1
  • 2
  • 3
  • 4

@android.view.RemotableViewMethod
public void setImageBitmap(Bitmap bm) {
    // Hacky fix to force setImageDrawable to do a full setImageDrawable
    // instead of doing an object reference comparison
    mDrawable = null;
    if (mRecycleableBitmapDrawable == null) {
        mRecycleableBitmapDrawable = new BitmapDrawable(mContext.getResources(), bm);
    } else {
        mRecycleableBitmapDrawable.setBitmap(bm);
    }
    setImageDrawable(mRecycleableBitmapDrawable);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

@android.view.RemotableViewMethod(asyncImpl="setImageResourceAsync")
public void setImageResource(@DrawableRes int resId) {
    // The resource configuration may have changed, so we should always
    // try to load the resource even if the resId hasn't changed.
    final int oldWidth = mDrawableWidth;
    final int oldHeight = mDrawableHeight;

    updateDrawable(null);
    mResource = resId;
    mUri = null;

    resolveUri();

    if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
        requestLayout();
    }
    invalidate();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

我们看这几个比较常用的方法,其实最终收口都是调用了setImageDrawable方法,即便是传入了Bitmap,那么也会将其转换成一个Drawable对象并调用setImageDrawable,所以我们要做大图检测一定要找一个收口的地方,因此在setImageDrawable方法调用的时候,检测当前图片的大小是否超过了ImageView的大小,就能判断是否是一张大图了。

public class MyImageView  extends ImageView {

    public MyImageView(Context context) {
        super(context);
    }

    public MyImageView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public MyImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public MyImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public void setImageDrawable(@Nullable Drawable drawable) {
        super.setImageDrawable(drawable);
        //容器的大小
        int widthContainer = this.getWidth();
        int heightContainer = this.getHeight();

        if (drawable != null){
            //获取图片的大小
            int intrinsicWidth = drawable.getIntrinsicWidth();
            int intrinsicHeight = drawable.getIntrinsicHeight();

            //只要有一个方向超了,都会报警
            if (intrinsicWidth > widthContainer * 2 || intrinsicHeight > heightContainer * 2 ){
                Log.e("BigViewCheck","BigViewCheck | checkIsBigView | $drawable $intrinsicWidth * $intrinsicHeight is bigView");
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

所以在ImageView执行setImageDrawable方法的时候,通过字节码插桩的形式插入这个方法中的代码,就可以实现大图的检测。

2.3 大图检测插件实现

大图检测的插件:

public class ViewCheckPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        AppExtension extension = project.getExtensions().getByType(AppExtension.class);
        if (extension != null){
            extension.registerTransform(new ViewCheckTransform());
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

2.3.1 Transform的实现

public class ViewCheckTransform  extends Transform {
    @Override
    public String getName() {
        return "ViewCheckTransform";
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }

    @Override
    public Set<QualifiedContent.Scope> getScopes() {
        HashSet hashSet = new HashSet();
        hashSet.add(QualifiedContent.Scope.PROJECT);
        hashSet.add(QualifiedContent.Scope.SUB_PROJECTS);
        hashSet.add(QualifiedContent.Scope.EXTERNAL_LIBRARIES);
        return hashSet;
    }

    @Override
    public boolean isIncremental() {
        return false;
    }

    @Override
    public void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {

        System.out.println("ViewCheckTransform start transform");
        //需要对jar包处理,ImageView存在Android SDK的jar包里
        inputs.forEach(new Consumer<TransformInput>() {
            @Override
            public void accept(TransformInput transformInput) {
                //文件夹交给下一级Transform即可,DirectoryInput仅限于我们自己的工程下的目录
                transformInput.getDirectoryInputs().forEach(new Consumer<DirectoryInput>() {
                    @Override
                    public void accept(DirectoryInput directoryInput) {
                        File dest = outputProvider.getContentLocation(directoryInput.getName(),
                                directoryInput.getContentTypes(),
                                directoryInput.getScopes(), Format.DIRECTORY);
                        try {
                            FileUtils.copyDirectory(directoryInput.getFile(),dest);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                });

                transformInput.getJarInputs().forEach(new Consumer<JarInput>() {
                    @Override
                    public void accept(JarInput jarInput) {
                        findClass(jarInput.getFile());
                        File dest = outputProvider.getContentLocation(jarInput.getName(), jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR);
                        try {
                            FileUtils.copyFile(jarInput.getFile(),dest);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                });
            }
        });
    }

    private void findClass(File file){
        if (file.isDirectory()){
            for (File temp:file.listFiles()){
                findClass(temp);
            }
        }else {
            //如果是文件,判断是不是ImageView
            handleFile(file);
        }
    }

    private void handleFile(File file) {
        System.out.println("ViewCheckTransform | handleFile | "+file);

    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81

既然我们想要找到ImageView,那么对于getDirectoryInputs,它只限于在我们自己写的工程代码中进行插桩处理,所以需要对getJarInputs中拿到的全部的jar包进行处理,最终生成一个新的jar包(对ImageView做过处理),交给下一个Transform来处理。

2.3.2 jar包处理

因为现在看到网上对于jar包处理的文章很少,鉴于此我这里做一次比较详细的介绍,因为在日常的开发中可能不仅仅局限于我们对业务插桩,对于系统源码的Hook也会有。

当我们拿到每一个jar包之后,我们可以通过JarFile类来进行jar包文件的读取,从中获取是否存在我们想要的AppCompatImageView这个类的class文件。

private void handleFile(File file) {
    try {
        JarFile jarFile = new JarFile(file);
        Enumeration<JarEntry> entries = jarFile.entries();
        while (entries.hasMoreElements()) {
            JarEntry jarEntry = entries.nextElement();

            if (jarEntry.getName().startsWith("androidx/appcompat/widget/AppCompatImageView")){
                System.out.println("ViewCheckTransform | handleFile | jarEntry " + jarEntry);
                InputStream inputStream = jarFile.getInputStream(jarEntry);
                System.out.println("ViewCheckTransform | inputStream "+inputStream);
                //完成字节码插桩
                handleASM(inputStream);
            }
        }

    } catch (Exception exp) {

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在JarFile中,每个class文件都是一个JarEntry个体,我们可以通过获取JarEntry的name来判断,是否是androidx/appcompat/widget/AppCompatImageView这个类,如果获取到这类的class文件之后,可以通过getInputStream方法来获取class文件的输入流,进行字节码插桩。

private byte[] handleASM(InputStream inputStream) {
        //
        try {
            ClassReader classReader = new ClassReader(inputStream);
            ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
            classReader.accept(new ViewCheckVisitor(Opcodes.ASM9,classWriter),ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
            return classWriter.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

对于ClassVisitor,我这里就不再详细赘述了,主要就是用来遍历访问这个类中的所有方法,能够在这个类方法执行之前和执行之后,进行字节码的插入,代码如下:

public class ViewCheckVisitor extends ClassVisitor {

    public ViewCheckVisitor(int api) {
        super(api);
    }

    public ViewCheckVisitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        System.out.println("ViewCheckVisitor | visitMethod | name "+name);
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        return new ViewCheckAdapter(api, mv, access, name, descriptor);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

public class ViewCheckAdapter  extends AdviceAdapter {

    /**
     * Constructs a new {@link AdviceAdapter}.
     *
     * @param api           the ASM API version implemented by this visitor. Must be one of {@link
     *                      Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}.
     * @param methodVisitor the method visitor to which this adapter delegates calls.
     * @param access        the method's access flags (see {@link Opcodes}).
     * @param name          the method's name.
     * @param descriptor    the method's descriptor (see {@link Type Type}).
     */
    protected ViewCheckAdapter(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
        super(api, methodVisitor, access, name, descriptor);
    }

    @Override
    protected void onMethodEnter() {
        super.onMethodEnter();

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

从下面打印出来的日志就可以看到,我们之前提到的ImageView中的核心方法都可以访问到,我们主要就是访问setImageDrawable这个方法。

ViewCheckVisitor | visitMethod | name <init>
ViewCheckVisitor | visitMethod | name <init>
ViewCheckVisitor | visitMethod | name <init>
ViewCheckVisitor | visitMethod | name setImageResource
ViewCheckVisitor | visitMethod | name setImageDrawable
ViewCheckVisitor | visitMethod | name setImageBitmap
ViewCheckVisitor | visitMethod | name setImageURI
ViewCheckVisitor | visitMethod | name setBackgroundResource
ViewCheckVisitor | visitMethod | name setBackgroundDrawable
ViewCheckVisitor | visitMethod | name setSupportBackgroundTintList
ViewCheckVisitor | visitMethod | name getSupportBackgroundTintList
ViewCheckVisitor | visitMethod | name setSupportBackgroundTintMode
ViewCheckVisitor | visitMethod | name getSupportBackgroundTintMode
ViewCheckVisitor | visitMethod | name setSupportImageTintList
ViewCheckVisitor | visitMethod | name getSupportImageTintList
ViewCheckVisitor | visitMethod | name setSupportImageTintMode
ViewCheckVisitor | visitMethod | name getSupportImageTintMode
ViewCheckVisitor | visitMethod | name drawableStateChanged
ViewCheckVisitor | visitMethod | name hasOverlappingRendering
ViewCheckVisitor | visitMethod | name setImageLevel
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

先不着急插桩,先考虑一下,当完成字节码插桩之后,怎么替换jar包中的class文件呢?

首先先告诉大家,如果在jar包中修改了class文件,是不可以直接原路返回写进原先的jar包中,这样会破坏jar包的文件结构,因此需要新建一个jar包,然后完成jar的替换即可。

private File handleJarFile(File file) {

    try {
        //将jar包读写到内存中
        JarFile jarFile = new JarFile(file);

        //创建一个新的jar包
        File newJarFile = new File(file.getParentFile(), "temp_" + file.getName());
        System.out.println("ViewCheckTransform | newJarFile | name " + getName());
        if (newJarFile.exists()) newJarFile.delete();
        JarOutputStream jos =
                new JarOutputStream(new BufferedOutputStream(new FileOutputStream(newJarFile)));
        //读取jar包内容
        Enumeration<JarEntry> entries = jarFile.entries();
        while (entries.hasMoreElements()) {

            //获取jar包文件输入流
            JarEntry jarEntry = entries.nextElement();
            InputStream inputStream = jarFile.getInputStream(jarEntry);
            //先往新jar包里加Entry
            jos.putNextEntry(new JarEntry(jarEntry.getName()));

            //判断是否需要字节码插桩
            if (jarEntry.getName().startsWith("androidx/appcompat/widget/AppCompatImageView")) {
                System.out.println("ViewCheckTransform | handleFile | jarEntry " + jarEntry);
                System.out.println("ViewCheckTransform | inputStream " + inputStream);
                //完成字节码插桩
                byte[] bytes = handleASM(inputStream);
                jos.write(bytes);
                jos.flush();
                inputStream.close();
            } else {
                //如果不需要修改,那么就把entry写到新的jar里就行了
                jos.write(IOUtils.toByteArray(inputStream));
                inputStream.close();
            }

        }

        //当前jar包处理完成
        jarFile.close();
        jos.closeEntry();
        jos.flush();
        jos.close();

        return newJarFile;

    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

具体的逻辑,我这里就不再多讲了,伙伴们如果有疑问可以直接在评论区问,主要的思想就是通过创建一个新的jar包,然后将不需要插桩的文件流写入到新的jar包以及需要插桩的并且修改过后的文件写入新的jar包。

private File findClass(File file) {
    if (file.isDirectory()) {
        for (File temp : file.listFiles()) {
            findClass(temp);
        }
    } else {
        //如果是文件,判断是不是ImageView
        return handleJarFile(file);
    }
    return null;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

transformInput.getJarInputs().forEach(new Consumer<JarInput>() {
    @Override
    public void accept(JarInput jarInput) {
        File srcFile = findClass(jarInput.getFile());
        System.out.println("ViewCheckTransform | findClass | srcFile "+srcFile);
        File dest = outputProvider.getContentLocation(jarInput.getName(), jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR);
        try {
            FileUtils.copyFile(srcFile, dest);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

最终遍历完成所有的jar包之后,拿到新的jar包,将它扔给下一级的Transform来进行处理。

2.3.3 实现代码插桩逻辑

所有的插桩逻辑都是在onMethodEnter中,也就是在方法执行之前执行,这里先简单打一条日志,看是否是插桩成功 的。

@Override
protected void onMethodEnter() {
    super.onMethodEnter();
    visitLdcInsn("ViewCheck");
    visitLdcInsn("\u5f00\u542f\u63d2\u6869\u4e86");
    visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
    visitInsn(POP);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

通过日志看好像是成功了,但是并不知道是哪个页面调用了setImageDrawable方法。

2023-04-08 21:02:55.356 12309-12309/com.lay.dm E/ViewCheck: 开启插桩了
2023-04-08 21:02:56.172 12309-12309/com.lay.dm E/ViewCheck: 开启插桩了
2023-04-08 21:02:56.286 12309-12309/com.lay.dm E/ViewCheck: 开启插桩了
2023-04-08 21:02:56.648 12309-12309/com.lay.dm E/ViewCheck: 开启插桩了
2023-04-08 21:02:56.966 12309-12309/com.lay.dm E/ViewCheck: 开启插桩了
  • 1
  • 2
  • 3
  • 4
  • 5

通过前面我们写的一段需要注入的代码,最终生成的字节码文件如下:

   L0
    LINENUMBER 36 L0
    LDC "ViewCheck"
    LDC "\u5f00\u542f\u63d2\u6869\u4e86"
    INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I
    POP
   L1
    LINENUMBER 37 L1
    ALOAD 0
    INVOKEVIRTUAL com/lay/dm/MyImageView.getWidth ()I
    ISTORE 2
   L2
    LINENUMBER 38 L2
    ALOAD 0
    INVOKEVIRTUAL com/lay/dm/MyImageView.getHeight ()I
    ISTORE 3
   L3
    LINENUMBER 40 L3
    ALOAD 1
    IFNULL L4
   L5
    LINENUMBER 42 L5
    ALOAD 1
    INVOKEVIRTUAL android/graphics/drawable/Drawable.getIntrinsicWidth ()I
    ISTORE 4
   L6
    LINENUMBER 43 L6
    ALOAD 1
    INVOKEVIRTUAL android/graphics/drawable/Drawable.getIntrinsicHeight ()I
    ISTORE 5
   L7
    LINENUMBER 46 L7
    ILOAD 4
    ILOAD 2
    ICONST_2
    IMUL
    IF_ICMPGT L8
    ILOAD 5
    ILOAD 3
    ICONST_2
    IMUL
    IF_ICMPLE L4
   L8
    LINENUMBER 47 L8
   FRAME FULL [com/lay/dm/MyImageView android/graphics/drawable/Drawable I I I I] []
    LDC "BigViewCheck"
    LDC "BigViewCheck | checkIsBigView | $drawable $intrinsicWidth * $intrinsicHeight is bigView"
    INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I
    POP
   L4
    LINENUMBER 50 L4
   FRAME FULL [com/lay/dm/MyImageView android/graphics/drawable/Drawable I I] []
    RETURN
   L9
    LOCALVARIABLE intrinsicWidth I L6 L4 4
    LOCALVARIABLE intrinsicHeight I L7 L4 5
    LOCALVARIABLE this Lcom/lay/dm/MyImageView; L0 L9 0
    LOCALVARIABLE drawable Landroid/graphics/drawable/Drawable; L0 L9 1
    LOCALVARIABLE widthContainer I L2 L9 2
    LOCALVARIABLE heightContainer I L3 L9 3
    MAXSTACK = 3
    MAXLOCALS = 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

其实这里面有一个特别坑的地方,我大概花了一天的时间,才发现其中的问题,我们看一下L3中的这段字节码,

   L3
    LINENUMBER 40 L3
    ALOAD 1
    IFNULL L4
  • 1
  • 2
  • 3
  • 4

这段字节码代表,如果var1,就是方法中传入的第一个参数为空,那么就跳转到L4中。

   L4
    LINENUMBER 50 L4
   FRAME FULL [com/lay/dm/MyImageView android/graphics/drawable/Drawable I I] []
    RETURN
  • 1
  • 2
  • 3
  • 4

像FRAME FULL这种需要计算栈帧的,其实可以在ClassWriter中选择COMPUTE_MAXS或者COMPUTE_FRAME,后者包括前者的功能,就不需要计算栈帧,而是会帮你自定计算。

然后L4中最后一个字节码是RETURN,坑就在这里,因为是往ImageView的源码中插入代码,其实在setImageDrawable方法中也存在一些源码,我们只是在方法开始的时候插入代码,因此在ASM插桩的时候,我调用了visitInsn(RETURN),结果发现setImageDrawable源码中的代码没有了,只有插入的代码

后来在查资料的时候发现,原来调用visitInsn(RETURN)是会清除方法体中的代码,才导致系统的源码找不见了,终于解决了我心中的郁闷,伙伴们在碰到这种情况的时候,对于RETURN可以视情况不用处理。

@Override
protected void onMethodEnter() {
    super.onMethodEnter();

    visitLdcInsn("ViewCheck");
    visitLdcInsn("\u5f00\u542f\u63d2\u6869\u4e86");
    visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
    visitInsn(POP);

    visitVarInsn(ALOAD, 0);
    visitMethodInsn(INVOKEVIRTUAL, "android/widget/ImageView", "getWidth", "()I");
    visitVarInsn(ISTORE, 2);

    visitVarInsn(ALOAD, 0);
    visitMethodInsn(INVOKEVIRTUAL, "android/widget/ImageView", "getHeight", "()I");
    visitVarInsn(ISTORE, 3);

    visitVarInsn(ALOAD, 1);
    Label nullLabel = new Label();
    visitJumpInsn(IFNULL, nullLabel);

    visitVarInsn(ALOAD, 1);
    visitMethodInsn(INVOKEVIRTUAL, "android/graphics/drawable/Drawable", "getIntrinsicWidth", "()I");
    visitVarInsn(ISTORE, 4);

    visitVarInsn(ALOAD, 1);
    visitMethodInsn(INVOKEVIRTUAL, "android/graphics/drawable/Drawable", "getIntrinsicHeight", "()I");
    visitVarInsn(ISTORE, 5);

    visitVarInsn(ILOAD, 4);
    visitVarInsn(ILOAD, 2);
    visitInsn(ICONST_2);
    visitInsn(IMUL);
    Label printLabel = new Label();
    visitJumpInsn(IF_ICMPGT, printLabel);
    visitVarInsn(ILOAD, 5);
    visitVarInsn(ILOAD, 3);
    visitInsn(ICONST_2);
    visitInsn(IMUL);
    visitJumpInsn(IF_ICMPLE, nullLabel);

    visitLabel(printLabel);
    visitLdcInsn("BigViewCheck");
    visitLdcInsn("BigViewCheck | checkIsBigView | $drawable $intrinsicWidth * $intrinsicHeight is bigView");
    visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
    visitInsn(POP);

    visitLabel(nullLabel);

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

上面就是整个插桩的代码,其实这个算是比较复杂的了,如果掌握了这些场景中的代码,后续基本上所有的问题就都不是问题了,我们看下插桩后,系统AppCompatImageView的class文件是什么样的。

public void setImageDrawable(@Nullable Drawable var1) {
    Log.e("ViewCheck", "开启插桩了");
    int var2 = this.getWidth();
    int var3 = this.getHeight();
    if (var1 != null) {
        int var4 = var1.getIntrinsicWidth();
        int var5 = var1.getIntrinsicHeight();
        if (var4 > var2 * 2 || var5 > var3 * 2) {
            Log.e("BigViewCheck", "BigViewCheck | checkIsBigView | $drawable $intrinsicWidth * $intrinsicHeight is bigView");
        }
    }

    if (this.mImageHelper != null && var1 != null && !this.mHasLevel) {
        this.mImageHelper.obtainLevelFromDrawable(var1);
    }

    super.setImageDrawable(var1);
    if (this.mImageHelper != null) {
        this.mImageHelper.applySupportImageTint();
        if (!this.mHasLevel) {
            this.mImageHelper.applyImageLevel();
        }
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

我们可以看到,在setImageDrawable中,我们插入的代码已经生效了。

但这里有一个问题就是,如果在setImageDrawable的时候,去获取容器的宽高,这个时候,拿到的可能是0,因为view还没有完全渲染完成,因此最好调用View # post,完成宽高的获取。

其实这篇文章更多的是介绍一个思路吧,如何去往系统sdk中的方法中进行字节码插桩,对于TransformInputs # jarInputs 的处理,系统jar包的替换等,如果想要获取其他场景中的数据,可以自行扩展。


Android 学习手册推荐

Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题锦:https://qr18.cn/CgxrRy
音视频面试题锦:https://qr18.cn/AcV6Ap

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/341304
推荐阅读
相关标签
  

闽ICP备14008679号