说到App的体积缩减,大家首先能想到的是一些比较常规的方式:
- 图片压缩
- 配置 ndk的abiFilter
- 配置resConfigs
- 代码混淆
- 资源混淆
- 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文件删除,达到减少字段及包大小的目的。