美文网首页
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