說到App的體積縮減,大家首先能想到的是一些比較常規(guī)的方式:
- 圖片壓縮
- 配置 ndk的abiFilter
- 配置resConfigs
- 代碼混淆
- 資源混淆
- xxxx
那么今天各谚,我將和大家一起探索一種新型的體積縮減方案:R文件刪除
R文件的一些特征:
R文件也是會像resource莺债、assets這些資源一樣進(jìn)行合并的滋觉,比如說是A模塊依賴B模塊,那么A在編譯期間生成的R文件中就包含了B模塊中生成的R文件中的值齐邦,所以在app模塊的R文件中椎侠,它涵蓋了項目中所有的R文件的值,因此我們可以將library或者aar中的R文件進(jìn)行刪除措拇,刪除這些R文件還有一個比較好的效果就是它大大減少了應(yīng)用中的字段數(shù)量
拿上述的圖片壓縮來減少包大小體積來說我纪,應(yīng)該怎么實現(xiàn)呢?最簡單粗暴的方式就是手動將項目中用到的圖片全部手動壓一遍丐吓,再不就是利用Android studio自帶的轉(zhuǎn)webp功能將圖片轉(zhuǎn)成webp格式的浅悉,這樣也沒啥問題,但是有些缺點券犁,那就是我引入的一些aar中的圖片术健,應(yīng)該如何做到刪除呢?
解決方案之一就是自定義gradle 插件在編譯期間進(jìn)行圖片壓縮
今天不拿圖片壓縮來舉例子族操,圖片壓縮這個例子應(yīng)該是屬于自定義task苛坚,今天的主角是自定義transform,雖然它也屬于一個task色难,但是代碼層面稍稍有些不一樣
在這里我就不介紹transform是什么東西了泼舱,不熟悉的同學(xué)科學(xué)上網(wǎng)搜搜也能知道個大概啦
常規(guī)的支持增量的自定義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時二次判斷來決策當(dāng)前是否是增量的
*/
@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);
}
/**
* 利用一些字節(jié)碼工具來動態(tài)改變類的行為
*/
private byte[] insertCodeToConstructors(InputStream inputStream) throws IOException {
//1. 構(gòu)建ClassReader對象
ClassReader classReader = new ClassReader(inputStream);
//2. 構(gòu)建ClassVisitor的實現(xiàn)類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方法拿到完整的字節(jié)流
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);
}
}
}
在這個例子中冒掌,我們所需要做的不是去動態(tài)更改class文件,而是刪除一些文件蹲盘,這更簡單了:
1.
app模塊的代碼一般是屬于DirectoryInput類型的股毫,一般情況下該類型的我們不做處理,因為這里面的R文件是應(yīng)用中所有的R引用了召衔,保存即可
2.
對于jarInput類型的輸入铃诬,我們需要作出如下的功能和判斷:
· 當(dāng)前不包含R文件,那么我們需要對該類中的R引用進(jìn)行替換
· 包含了R文件,但是該類是配置在了白名單中趣席,那么該類就不作變換
· 包含了R文件兵志,但是該R文件是以app模塊中的包名開頭,也無需作出變換
· 可以做一個開關(guān)宣肚,在打debug包時想罕,不做R文件的刪除,以此來節(jié)省部分編譯時間
差不多就是這么多霉涨,下面開始寫代碼了:
定義如下實體類:
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文件的標(biāo)識
*
* @param check 待檢測的字符串
* @return 有標(biāo)識的話返回true
*/
private static boolean hasR(String check) {
for (String s : R) {
if (check.contains(s)) {
return true;
}
}
return false;
}
}
經(jīng)過上述Transform + ASM處理,可以對aar及l(fā)ibrary中打包生成的R文件刪除嵌纲,達(dá)到減少字段及包大小的目的俘枫。