Android 大图检测插件的落地

news2024/10/5 15:22:57

作者: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();
    }
}

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

@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);
}

@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();
}

我们看这几个比较常用的方法,其实最终收口都是调用了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");
            }
        }
    }
}

所以在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());
        }
    }
}

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);

    }

}

既然我们想要找到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) {

    }
}

在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();
        }
    }

对于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);
    }
}

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();

    }
}

从下面打印出来的日志就可以看到,我们之前提到的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

先不着急插桩,先考虑一下,当完成字节码插桩之后,怎么替换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;
    }

}

具体的逻辑,我这里就不再多讲了,伙伴们如果有疑问可以直接在评论区问,主要的思想就是通过创建一个新的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;
}

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();
        }
    }
});

最终遍历完成所有的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);
}

通过日志看好像是成功了,但是并不知道是哪个页面调用了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: 开启插桩了

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

   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

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

   L3
    LINENUMBER 40 L3
    ALOAD 1
    IFNULL L4

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

   L4
    LINENUMBER 50 L4
   FRAME FULL [com/lay/dm/MyImageView android/graphics/drawable/Drawable I I] []
    RETURN

像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);

}

上面就是整个插桩的代码,其实这个算是比较复杂的了,如果掌握了这些场景中的代码,后续基本上所有的问题就都不是问题了,我们看下插桩后,系统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();
        }
    }

}

我们可以看到,在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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/419265.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

前端视角-https总结

1.http存在的问题 1.1可能被窃听 HTTP 本身不具备加密的功能,HTTP 报文使用明文方式发送互联网是由联通世界各个地方的网络设施组成,所有发送和接收经过某些设备的数据都可能被截获或窥视。(例如TCP/IP抓包工具:Wireshark),即使经过加密处理,也会被窥视是通信内容,只是可能很…

在 Flutter 多人视频通话中实现虚拟背景、美颜与空间音效

前言 在之前的「基于声网 Flutter SDK 实现多人视频通话」里&#xff0c;我们通过 Flutter 声网 SDK 完美实现了跨平台和多人视频通话的效果&#xff0c;那么本篇我们将在之前例子的基础上进阶介绍一些常用的特效功能&#xff0c;包括虚拟背景、色彩增强、空间音频、基础变声…

HBase高手之路4-Shell操作

文章目录HBase高手之路3—HBase的shell操作一、hbase的shell命令汇总二、需求三、表的操作1&#xff0e;进入shell命令行2&#xff0e;创建表3&#xff0e;查看表的定义4&#xff0e;列出所有的表5&#xff0e;删除表1)禁用表2)启用表3)删除表四、数据的操作1&#xff0e;添加数…

TensorFlow 深度学习实战指南:1~5 全

原文&#xff1a;Hands-on Deep Learning with TensorFlow 协议&#xff1a;CC BY-NC-SA 4.0 译者&#xff1a;飞龙 本文来自【ApacheCN 深度学习 译文集】&#xff0c;采用译后编辑&#xff08;MTPE&#xff09;流程来尽可能提升效率。 不要担心自己的形象&#xff0c;只关心如…

【通义千问】继ChatGPT爆火后,阿里云的大模型“通义千问”它终于来了

通义千问一、通义千问名字的由来二、通义千问和ChatGPT有什么区别呢&#xff1f;三、如何申请体验通义千问呢&#xff1f;四、未来通义千问能称为中国版的ChatGPT吗&#xff1f;五、通义千问什么时候正式发布呢&#xff1f;一、通义千问名字的由来 通义千问顾名思义&#xff0…

作物杂交——蓝桥杯20年省赛(JAVA)

题目链接&#xff1a; 用户登录https://www.lanqiao.cn/problems/506/learning/?page2&first_category_id1&sortstudents_count 题目描述 作物杂交是作物栽培中重要的一步。已知有 N 种作物 (编号 1 至 N )&#xff0c;第 i 种作物从播种到成熟的时间为 Ti​。作物…

少儿编程 电子学会图形化 scratch编程等级考试四级真题答案解析(判断题)2022年12月

2022年12月scratch编程等级考试四级真题 判断题(共10题,每题2分,共20分) 16、点击绿旗,反复按下空格键,可以使变量a的值在0和1之间反复变化 答案:对 考点分析:考查积木综合使用,重点考查变量积木的使用,按一下空格键,a变量值会改变5次,0-1-0-1-0-1,按第二下…

budibase <2.4.3 存在 ssrf 漏洞(CVE-2023-29010)

漏洞描述 budibase 是一个开源的低代码平台&#xff0c;元数据端点(metadata endpoint)是Budibase提供的一个REST API端点&#xff0c;用于访问应用程序的元数据信息。 budibase 2.4.3之前版本中存在 ssrf 漏洞&#xff0c;该漏洞可能影响 Budibase 自主托管的用户&#xff0…

安利安利-向大家推荐一个超级牛的etcd管理工具-EtcdKeeperFyne

etcd介绍 关于etcd的介绍大家可以看下这篇文章 etcd 开源仓库地址&#xff1a;EtcdKeeperFyne EtcdKeeperFyne 今天主要是向大家推荐一款使用起来特别方便的Etcd管理工具 EtcdKeeperFyne&#xff0c;具体运行起来的界面如下&#xff1a; 推荐原因 使用简单安装简单&…

卷积层输出尺寸计算 / 感受野尺寸计算

卷积层输出尺寸计算 输入图像a*a, 卷积核大小b*b, stride c, padding d 输出图像的尺寸&#xff1a;[(a - b 2d) // c] 1 (a - b 2d) 表示在输入图像两侧填充 d 个像素后&#xff0c;窗口在输入图像上最多能移动的距离&#xff0c;再加上 1 表示最后一个窗口的右侧边界…

博客文章效果

学习风宇blog md文档转html&#xff08;markdown-it的使用&#xff09;语法高亮、行号、一键复制toc生成目录sticky粘性定位 <style lang"scss"> import url(//at.alicdn.com/t/c/font_4004562_9v94jccafmc.css); import url(https://fonts.font.im/css?fam…

DFIG控制8: 不平衡电网下的网侧变换器控制

DFIG控制8&#xff1a; 不平衡电网下的网侧变换器控制。主要是添加网侧变换器的负序分量控制器。 本文基于教程的第8部分&#xff1a;DFIM Tutorial 8 - Asymmetrical Voltage Dips Analysis in DFIG based WT: Grid Side Converter Control 控制策略简介 来自&#xff1a;G…

过滤器(Filter)与拦截器(Interceptor)区别

1 过滤器&#xff08;Filter&#xff09; Servlet 中的过滤器 Filter 实现了 javax.servlet.Filter 接口的服务器端程序&#xff0c;主要用途是设置字符集&#xff08;CharacterEncodingFilter&#xff09;、控制权限、控制转向、用户是否已经登陆、有没有权限访问该页面等。 …

springboot配置跨域问题

近期自己搭建项目时&#xff0c;遇到一个跨域问题。我们以前项目解决跨域是在controller上加一个跨域注解CrossOrigin(allowCredentials "true")&#xff0c;很方便。但是在我自己搭建的项目中&#xff0c;启动时竟然报错了&#xff0c;错误如下&#xff1a; When …

图的传递闭包

给定一个有向图,对于给定图中的所有顶点对(i, j),找出一个顶点j是否可从另一个顶点i到达。这里的可达性是指从顶点i到j有一条路径。可达性矩阵称为图的传递闭包。 例如,考虑下面的图表 上述图的传递闭包为 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 该图以邻接矩阵的形式给出,…

抛弃 TCP 和 QUIC 的 HTTP

下班路上发了一则朋友圈&#xff1a; 周四听了斯坦福老教授 John Ousterhout 关于 Homa 的分享&#xff0c;基本重复了此前那篇 It’s Time To Rep… 的格调&#xff0c;花了一多半时间喷 TCP… Ousterhout 关于 Homa 和 TCP 之间的论争和论证&#xff0c;诸多反复回执&…

DAY15|102.二叉树的层序遍历。。。。等层序遍历的十道题

102.二叉树的层序遍历 代码随想录中的这题java和c不太一样 class Solution {public List<List<Integer>> resList new ArrayList<List<Integer>>();public List<List<Integer>> levelOrder(TreeNode root) {checkFun01(root,0);return …

NVIDIA- cuSPARSE(四)

cuSPARSE logging 日志记录机制&#xff0c; 可以通过在启动目标应用程序之前设置一下环境变量来启动cuSPARSE日志记录机制&#xff1a; CUSPARSE_LOG_LEVEL<level> level的取值&#xff1a; 0 Off 日志记录关闭1 Error只有报错会被记录2Trace启动CUDA内核的API调用将记…

网络应用程序设计(idea版)——实验四:会话管理

目录 实验预习报告 一、实验目的 二、实验原理 三、实验预习内容 实验报告 一、实验目的 二、实验要求 三、实验内容与步骤 实验预习报告 一、实验目的 1. 了解Web服务器对客户会话跟踪的各种方法&#xff1b; 2. 重点掌握使用HttpSession对象跟踪会话的方法&#…

OCAF——数据结构机制 Sample2

Email:dev_as@163.com Another example is the application for designing table lamps. The first label is allocated to the lamp unit. The tree definition of Lamp The root label cannot have brother labels. :[Root : (0)],根节点没有兄弟节点 Consequently, var…