美文网首页
App体积缩减(R文件删除)-自定义transform

App体积缩减(R文件删除)-自定义transform

作者: kunio | 来源:发表于2021-03-03 20:51 被阅读0次

说到App的体积缩减,大家首先能想到的是一些比较常规的方式:

  1. 图片压缩
  2. 配置 ndk的abiFilter
  3. 配置resConfigs
  4. 代码混淆
  5. 资源混淆
  6. xxxx

那么今天,我将和大家一起探索一种新型的体积缩减方案:R文件删除
R文件的一些特征:
R文件也是会像resource、assets这些资源一样进行合并的,比如说是A模块依赖B模块,那么A在编译期间生成的R文件中就包含了B模块中生成的R文件中的值,所以在app模块的R文件中,它涵盖了项目中所有的R文件的值,因此我们可以将library或者aar中的R文件进行删除,删除这些R文件还有一个比较好的效果就是它大大减少了应用中的字段数量

拿上述的图片压缩来减少包大小体积来说,应该怎么实现呢?最简单粗暴的方式就是手动将项目中用到的图片全部手动压一遍,再不就是利用Android studio自带的转webp功能将图片转成webp格式的,这样也没啥问题,但是有些缺点,那就是我引入的一些aar中的图片,应该如何做到删除呢?
解决方案之一就是自定义gradle 插件在编译期间进行图片压缩

今天不拿图片压缩来举例子,图片压缩这个例子应该是属于自定义task,今天的主角是自定义transform,虽然它也属于一个task,但是代码层面稍稍有些不一样

在这里我就不介绍transform是什么东西了,不熟悉的同学科学上网搜搜也能知道个大概啦

常规的支持增量的自定义transform如下所示:

package com.kunio.plugin;

import com.android.build.api.transform.DirectoryInput;
import com.android.build.api.transform.Format;
import com.android.build.api.transform.JarInput;
import com.android.build.api.transform.QualifiedContent;
import com.android.build.api.transform.Status;
import com.android.build.api.transform.Transform;
import com.android.build.api.transform.TransformInput;
import com.android.build.api.transform.TransformInvocation;
import com.android.build.api.transform.TransformOutputProvider;
import com.android.build.gradle.internal.pipeline.TransformManager;
import com.android.ide.common.internal.WaitableExecutor;
import com.android.utils.FileUtils;
import com.edu.assets.merge.pre.LottieClassVisitor;

import org.apache.commons.io.IOUtils;
import org.gradle.api.Project;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;

public class KunioTransform extends Transform {
    private WaitableExecutor executor = WaitableExecutor.useGlobalSharedThreadPool();
    private static final String NAME = "AssetsLottie";
    private final Project project;

    public KunioTransform(Project project) {
        this.project = project;
    }

    /**
     * @return 返回transform 的名称
     * <p>
     * 最后会有transformClassesWithXxxForDebug、transformClassesWithXxxForRelease等task
     */
    @Override
    public String getName() {
        return NAME;
    }

    /**
     * @return 返回需要处理的输入类型,这里我们处理class文件
     */
    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }

    /**
     * @return 返回处理的作用域范围,我们这里处理整个项目中的class文件,包括aar中的
     */
    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    /**
     * @return 是否支持增量,需要在transform时二次判断来决策当前是否是增量的
     */
    @Override
    public boolean isIncremental() {
        return true;
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws InterruptedException, IOException {
        boolean isIncremental = transformInvocation.isIncremental();
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
        if (!isIncremental) {
            outputProvider.deleteAll();
        }
        Collection<TransformInput> inputs = transformInvocation.getInputs();
        for (TransformInput input : inputs) {
            Collection<JarInput> jarInputs = input.getJarInputs();
            for (JarInput jarInput : jarInputs) {
                executor.execute(() -> {
                    processJarInputIncremental(jarInput, outputProvider, isIncremental);
                    return null;
                });
            }

            Collection<DirectoryInput> directoryInputs = input.getDirectoryInputs();
            for (DirectoryInput directoryInput : directoryInputs) {
                executor.execute(() -> {
                    processDirectoryInputIncremental(directoryInput, outputProvider, isIncremental);
                    return null;
                });
            }
        }
        executor.waitForTasksWithQuickFail(true);
    }

    private void processDirectoryInputIncremental(DirectoryInput directoryInput, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException {
        File inputDir = directoryInput.getFile();
        File outputDir = outputProvider.getContentLocation(
                directoryInput.getName(),
                directoryInput.getContentTypes(),
                directoryInput.getScopes(),
                Format.DIRECTORY);
        if (isIncremental) {
            Set<Map.Entry<File, Status>> entries = directoryInput.getChangedFiles().entrySet();
            for (Map.Entry<File, Status> entry : entries) {
                File file = entry.getKey();
                File destFile = new File(file.getAbsolutePath().replace(inputDir.getAbsolutePath(), outputDir.getAbsolutePath()));
                Status status = entry.getValue();
                switch (status) {
                    case ADDED:
                        FileUtils.mkdirs(destFile);
                        FileUtils.copyFile(file, destFile);
                        break;
                    case CHANGED:
                        FileUtils.deleteIfExists(destFile);
                        FileUtils.mkdirs(destFile);
                        FileUtils.copyFile(file, destFile);
                        break;
                    case REMOVED:
                        FileUtils.deleteIfExists(destFile);
                        break;
                    case NOTCHANGED:
                        break;
                }
            }
        } else {
            FileUtils.copyDirectory(directoryInput.getFile(), outputDir);
        }
    }

    private void processJarInputIncremental(JarInput jarInput, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException {
        File dest = outputProvider.getContentLocation(
                jarInput.getFile().getAbsolutePath(),
                jarInput.getContentTypes(),
                jarInput.getScopes(),
                Format.JAR);
        if (isIncremental) {
            //处理增量编译
            switch (jarInput.getStatus()) {
                case NOTCHANGED:
                    break;
                case ADDED:
                    processJarInput(jarInput, dest);
                    break;
                case CHANGED:
                    //处理有变化的
                    FileUtils.deleteIfExists(dest);
                    processJarInput(jarInput, dest);
                    break;
                case REMOVED:
                    //移除Removed
                    FileUtils.deleteIfExists(dest);
                    break;
            }
        } else {
            //不处理增量编译
            processJarInput(jarInput, dest);
        }
    }

    private void processJarInput(JarInput jarInput, File dest) throws IOException {
        String name = jarInput.getName();
//        com.airbnb.android:lottie:3.6.1
//        androidx.cardview:cardview:1.0.0
//        androidx.coordinatorlayout:coordinatorlayout:1.1.0
//        androidx.fragment:fragment:1.1.0
//        androidx.constraintlayout:constraintlayout:2.0.4
//        androidx.appcompat:appcompat:1.2.0
//        do something
        realProcessJarInput(jarInput);
        FileUtils.copyFile(jarInput.getFile(), dest);
    }

    private void realProcessJarInput(JarInput jarInput) throws IOException {
        File file = jarInput.getFile();
        File tempJar = new File(file.getParentFile(), file.getName() + ".temp");
        JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tempJar));
        JarFile jar = new JarFile(file);
        Enumeration<JarEntry> entries = jar.entries();
        boolean changed = false;
        while (entries.hasMoreElements()) {
            JarEntry jarEntry = entries.nextElement();
            InputStream inputStream = jar.getInputStream(jarEntry);
            jarOutputStream.putNextEntry(new ZipEntry(jarEntry.getName()));
//            com/airbnb/lottie/L.class
//            com/airbnb/lottie/L.class
//            com/airbnb/lottie/Lottie.class
//            com/airbnb/lottie/Lottie.class
//            com/airbnb/lottie/LottieAnimationView.class
            if (true) {
                // 如果是需要处理这个class文件
                changed = true;
                byte[] bytes = insertCodeToConstructors(inputStream);
                jarOutputStream.write(bytes);
            } else {
                jarOutputStream.write(IOUtils.toByteArray(inputStream));
            }
            inputStream.close();
            jarOutputStream.closeEntry();
        }
        jar.close();
        jarOutputStream.close();
        if (changed) {
            FileUtils.delete(file);
            tempJar.renameTo(file);
        } else {
            FileUtils.delete(tempJar);
        }
//        FileUtils.delete(tempJar);
    }


    /**
     * 利用一些字节码工具来动态改变类的行为
     */
    private byte[] insertCodeToConstructors(InputStream inputStream) throws IOException {

        //1. 构建ClassReader对象
        ClassReader classReader = new ClassReader(inputStream);
        //2. 构建ClassVisitor的实现类ClassWriter
        ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
        KunioClassVisitor visitor = new KunioClassVisitor(Opcodes.ASM6, classWriter);
        classReader.accept(visitor, ClassReader.EXPAND_FRAMES);
        //4. 通过classWriter对象的toByteArray方法拿到完整的字节流
        return classWriter.toByteArray();
    }
}


public class KunioClassVisitor extends ClassVisitor {
    public KunioClassVisitor(int api) {
        super(api);
    }

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

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
        if ("init".equals(name) && "(Landroid/util/AttributeSet;I)V".equals(descriptor)) {
//            System.out.println("access = " + access);
//            System.out.println("name = " + name);
//            System.out.println("descriptor = " + descriptor);
//            System.out.println("signature = " + signature);
            return new initMethodVisitor(api, methodVisitor,access,name,descriptor);
        } else {
            return methodVisitor;
        }
    }

    static class initMethodVisitor extends AdviceAdapter {
        initMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
            super(api, methodVisitor, access, name, descriptor);
        }

        @Override
        protected void onMethodExit(int opcode) {
            super.onMethodExit(opcode);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESTATIC, "com/edu/assets/lottie/bitmap/delegate/BitmapDelegate", "setDelegate", "(Lcom/airbnb/lottie/LottieAnimationView;)V", false);
        }
    }
}

在这个例子中,我们所需要做的不是去动态更改class文件,而是删除一些文件,这更简单了:

1.

app模块的代码一般是属于DirectoryInput类型的,一般情况下该类型的我们不做处理,因为这里面的R文件是应用中所有的R引用了,保存即可

2.

对于jarInput类型的输入,我们需要作出如下的功能和判断:
· 当前不包含R文件,那么我们需要对该类中的R引用进行替换
· 包含了R文件,但是该类是配置在了白名单中,那么该类就不作变换
· 包含了R文件,但是该R文件是以app模块中的包名开头,也无需作出变换
· 可以做一个开关,在打debug包时,不做R文件的删除,以此来节省部分编译时间

差不多就是这么多,下面开始写代码了:
定义如下实体类:

 class UnifyRExtension {
     // app 模块包名
    public String appPackageName;
    // 类白名单包名,处于此包下的类不处理
    public List<String> whitePackage;
    // debug模式下跳过处理
    public boolean skipDebug = true
}

gradle plgin类:

public class KunioPlugin implements Plugin<Project> {
    private static final String CONFIG_NAME = "UnifyRExtension";

    @Override
    public void apply(@NotNull Project project) {
        boolean hasAppPlugin = project.getPlugins().hasPlugin("com.android.application");
        if (!hasAppPlugin) {
            throw new GradleException("this plugin can't use in library module");
        }
        AppExtension android = (AppExtension) project.getExtensions().findByName("android");
        if (android == null) {
            throw new NullPointerException("application module not have \"android\" block!");
        }
        project.getExtensions().create(CONFIG_NAME, UnifyRExtension.class);
        android.registerTransform(new UnifyRTransform(project));
    }
}

transform:

package com.edu.android.plugin;

import com.android.build.api.transform.DirectoryInput;
import com.android.build.api.transform.Format;
import com.android.build.api.transform.JarInput;
import com.android.build.api.transform.QualifiedContent;
import com.android.build.api.transform.Status;
import com.android.build.api.transform.Transform;
import com.android.build.api.transform.TransformException;
import com.android.build.api.transform.TransformInput;
import com.android.build.api.transform.TransformInvocation;
import com.android.build.api.transform.TransformOutputProvider;
import com.android.build.gradle.internal.pipeline.TransformManager;
import com.android.ide.common.internal.WaitableExecutor;
import com.android.utils.FileUtils;

import org.apache.commons.io.IOUtils;
import org.gradle.api.Project;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;

public class UnifyRTransform extends Transform {
    private static final List<String> R = Arrays.asList(
            "R$xml",
            "R$transition",
            "R$styleable",
            "R$style",
            "R$string",
            "R$raw",
            "R$plurals",
            "R$mipmap",
            "R$menu",
            "R$layout",
            "R$interpolator",
            "R$integer",
            "R$id",
            "R$fraction",
            "R$font",
            "R$drawable",
            "R$dimen",
            "R$color",
            "R$bool",
            "R$attr",
            "R$array",
            "R$animator",
            "R$anim");
    private WaitableExecutor executor = WaitableExecutor.useGlobalSharedThreadPool();
    private static final String NAME = "UnifyR";
    private final Project project;
    private String appPackagePrefix;
    private List<String> whitePackages;

    public UnifyRTransform(Project project) {
        this.project = project;
    }

    @Override
    public String getName() {
        return NAME;
    }

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

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }

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

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        UnifyRExtension unifyRExtension = project.getExtensions().findByType(UnifyRExtension.class);
        boolean skipDebugUnifyR = transformInvocation.getContext().getVariantName().toLowerCase().contains("debug") && unifyRExtension.skipDebug;
        if (skipDebugUnifyR) {
            copyOnly(transformInvocation.getInputs(), transformInvocation.getOutputProvider());
            return;
        }
        appPackagePrefix = unifyRExtension.packageName.replace('.', '/') + '/';
        List<String> whitePackage = unifyRExtension.whitePackage;
        List<String> whites = new ArrayList<>();
        if (whitePackage != null) {
            for (String s : whitePackage) {
                whites.add(s.replace('.', '/'));
            }
        }
        whitePackages = new ArrayList<>(whites);

        boolean isIncremental = transformInvocation.isIncremental();
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
        if (!isIncremental) {
            outputProvider.deleteAll();
        }
        Collection<TransformInput> inputs = transformInvocation.getInputs();
        for (TransformInput input : inputs) {
            Collection<JarInput> jarInputs = input.getJarInputs();
            for (JarInput jarInput : jarInputs) {
                executor.execute(() -> {
                    processJarInputWithIncremental(jarInput, outputProvider, isIncremental);
                    return null;
                });
            }

            Collection<DirectoryInput> directoryInputs = input.getDirectoryInputs();
            for (DirectoryInput directoryInput : directoryInputs) {
                executor.execute(() -> {
                    processDirectoryInputWithIncremental(directoryInput, outputProvider, isIncremental);
                    return null;
                });
            }
        }
        executor.waitForTasksWithQuickFail(true);
    }

    private void processDirectoryInputWithIncremental(DirectoryInput directoryInput, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException {
        File inputDir = directoryInput.getFile();
        File outputDir = outputProvider.getContentLocation(
                directoryInput.getName(),
                directoryInput.getContentTypes(),
                directoryInput.getScopes(),
                Format.DIRECTORY);
        if (isIncremental) {
            Set<Map.Entry<File, Status>> entries = directoryInput.getChangedFiles().entrySet();
            for (Map.Entry<File, Status> entry : entries) {
                File file = entry.getKey();
                File destFile = new File(file.getAbsolutePath().replace(inputDir.getAbsolutePath(), outputDir.getAbsolutePath()));
                Status status = entry.getValue();
                switch (status) {
                    case ADDED:
                        FileUtils.mkdirs(destFile);
                        FileUtils.copyFile(file, destFile);
                        break;
                    case CHANGED:
                        FileUtils.deleteIfExists(destFile);
                        FileUtils.mkdirs(destFile);
                        FileUtils.copyFile(file, destFile);
                        break;
                    case REMOVED:
                        FileUtils.deleteIfExists(destFile);
                        break;
                    case NOTCHANGED:
                        break;
                }
            }
        } else {
            FileUtils.copyDirectory(directoryInput.getFile(), outputDir);
        }
    }

    private void processJarInputWithIncremental(JarInput jarInput, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException {
        File dest = outputProvider.getContentLocation(
                jarInput.getFile().getAbsolutePath(),
                jarInput.getContentTypes(),
                jarInput.getScopes(),
                Format.JAR);
        if (isIncremental) {
            //处理增量编译
            switch (jarInput.getStatus()) {
                case NOTCHANGED:
                    break;
                case ADDED:
                    processJarInput(jarInput, dest);
                    break;
                case CHANGED:
                    //处理有变化的
                    FileUtils.deleteIfExists(dest);
                    processJarInput(jarInput, dest);
                    break;
                case REMOVED:
                    //移除Removed
                    if (dest.exists()) {
                        FileUtils.delete(dest);
                    }
                    break;
            }
        } else {
            //不处理增量编译
            processJarInput(jarInput, dest);
        }
    }

    private void processJarInput(JarInput jarInput, File dest) throws IOException {
        processClass(jarInput);
        FileUtils.copyFile(jarInput.getFile(), dest);
    }

    private void processClass(JarInput jarInput) throws IOException {
        File file = jarInput.getFile();
        File tempJar = new File(file.getParentFile(), file.getName() + ".temp");
        JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tempJar));
        JarFile jar = new JarFile(file);
        Enumeration<JarEntry> entries = jar.entries();
        while (entries.hasMoreElements()) {
            byte[] destBytes = null;
            JarEntry jarEntry = entries.nextElement();
            InputStream inputStream = jar.getInputStream(jarEntry);
            String name = jarEntry.getName();
            if (name.endsWith(".class")) {
                boolean keep = false;
                for (String s : whitePackages) {
                    if (name.contains(s)) {
                        keep = true;
                        break;
                    }
                }
                if (keep) {
                    destBytes = IOUtils.toByteArray(inputStream);
                } else {
                    if (!hasR(name)) {
                        destBytes = unifyR(name, inputStream);
                    } else if (name.startsWith(appPackagePrefix)) {
                        destBytes = IOUtils.toByteArray(inputStream);
                    }
                }
            } else {
                destBytes = IOUtils.toByteArray(inputStream);
            }
            if (destBytes != null) {
                jarOutputStream.putNextEntry(new ZipEntry(jarEntry.getName()));
                jarOutputStream.write(destBytes);
                jarOutputStream.closeEntry();
            }
            inputStream.close();
        }
        jar.close();
        jarOutputStream.close();
        FileUtils.delete(file);
        tempJar.renameTo(file);
    }

    private byte[] unifyR(String entryName, InputStream inputStream) throws IOException {
        ClassReader cr = new ClassReader(inputStream);
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        ClassVisitor cv = new ClassVisitor(Opcodes.ASM6, cw) {

            @Override
            public MethodVisitor visitMethod(int access, String name, String desc,
                                             String signature, String[] exceptions) {
                MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
                return new MethodVisitor(Opcodes.ASM6, mv) {
                    @Override
                    public void visitFieldInsn(int opcode, String owner, String fName, String fDesc) {
                        if (hasR(owner) && !owner.contains(appPackagePrefix)) {
                            super.visitFieldInsn(opcode, appPackagePrefix + "R$" + owner.substring(owner.indexOf("R$") + 2), fName, fDesc);
                        } else {
                            super.visitFieldInsn(opcode, owner, fName, fDesc);
                        }
                    }
                };
            }

        };
        cr.accept(cv, ClassReader.EXPAND_FRAMES);
        return cw.toByteArray();
    }

    private static void copyOnly(Collection<TransformInput> inputs, TransformOutputProvider outputProvider) throws IOException {
        for (TransformInput input : inputs) {
            Collection<JarInput> jarInputs = input.getJarInputs();
            for (JarInput jarInput : jarInputs) {
                File dest = outputProvider.getContentLocation(
                        jarInput.getName(),
                        jarInput.getContentTypes(),
                        jarInput.getScopes(),
                        Format.JAR);
                FileUtils.copyFile(jarInput.getFile(), dest);
            }

            Collection<DirectoryInput> directoryInputs = input.getDirectoryInputs();
            for (DirectoryInput directoryInput : directoryInputs) {
                File dest = outputProvider.getContentLocation(
                        directoryInput.getName(),
                        directoryInput.getContentTypes(),
                        directoryInput.getScopes(),
                        Format.DIRECTORY);
                FileUtils.copyDirectory(directoryInput.getFile(), dest);
            }
        }
    }

    /**
     * 判断这个字符串里面有没有R文件的标识
     *
     * @param check 待检测的字符串
     * @return 有标识的话返回true
     */
    private static boolean hasR(String check) {
        for (String s : R) {
            if (check.contains(s)) {
                return true;
            }
        }
        return false;
    }
}

经过上述Transform + ASM处理,可以对aar及library中打包生成的R文件删除,达到减少字段及包大小的目的。

最后

1. whitePackage需要将androidx.constraintlayout作为白名单填入
2. packageName需要填写成为app模块的包名即可
2. 上述示例只是给出了transform与plugin代码,如何发布以及应用plugin需要自己查询相关资料

相关文章

网友评论

      本文标题:App体积缩减(R文件删除)-自定义transform

      本文链接:https://www.haomeiwen.com/subject/rkixqltx.html