背景
Android在接入各类渠道SDK的时候,常常会遇到需要继承三方Application的情况;网上几种方案:
- 利用代理模式实现多继承
- 利用gradle 不同的favor实现不同渠道打包
- 利用gradle插件,动态修改字节码
为了不维护多个风味,本文要分享的是第三种方案,直接通过自定义的标识去动态修改字节码实现不同渠道继承所需要的三方application类
具体实现
知识点包括:
- 自定义gradle插件
- transform api简单了解
- javaassit框架API 官方API文档 | 别人翻译的一些API文档
关键步骤:
- 自定义gradle插件
- 在自定义的transform类 找到要修改的application类
- 利用javaassit修改其父类
- 删除原来的class文件 (如果是在jar包则需要重新打包成jar)
引入依赖库
project-buildSrc module下引入三方库
implementation "commons-codec:commons-codec:1.11"
implementation "org.javassist:javassist:3.23.1-GA"
具体实现
参考注释
package com.hjl.plugin
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import javassist.ClassPool
import javassist.CtClass
import javassist.JarClassPath
import org.apachde.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.gradle.api.Project
/**
* reference https://github.com/Deemonser/JavassistDemo/blob/master/plugin/src/main/groovy/com/deemons/bus/JavassistTransform.groovy
*/
class ApplicationTransform extends Transform {
Project project
File originFile, targetFile, extractRoot
def targetName = "xxx"// 三方application类
def originName = "xxx" // 要替换父类为三方application的类
ApplicationTransform(Project project) {
this.project = project
}
@Override
String getName() {
return "applicationtrans"
}
/**
* 需要处理的数据类型,目前 ContentType 有六种枚举类型,通常我们使用比较频繁的有前两种:
* 1、CONTENT_CLASS:表示需要处理 java 的 class 文件。
* 2、CONTENT_JARS:表示需要处理 java 的 class 与 资源文件。
* 3、CONTENT_RESOURCES:表示需要处理 java 的资源文件。
* 4、CONTENT_NATIVE_LIBS:表示需要处理 native 库的代码。
* 5、CONTENT_DEX:表示需要处理 DEX 文件。
* 6、CONTENT_DEX_WITH_RESOURCES:表示需要处理 DEX 与 java 的资源文件。
*
* @return
*/
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
/**
* 表示 Transform 要操作的内容范围,目前 Scope 有五种基本类型:
* 1、PROJECT 只有项目内容
* 2、SUB_PROJECTS 只有子项目
* 3、EXTERNAL_LIBRARIES 只有外部库
* 4、TESTED_CODE 由当前变体(包括依赖项)所测试的代码
* 5、PROVIDED_ONLY 只提供本地或远程依赖项
* SCOPE_FULL_PROJECT 是一个 Scope 集合,包含 Scope.PROJECT, Scope.SUB_PROJECTS, Scope.EXTERNAL_LIBRARIES 这三项,即当前 Transform 的作用域包括当前项目、子项目以及外部的依赖库
*
* @return
*/
@Override
Set<QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
// 是否支持增量更新
// 如果返回 true,TransformInput 会包含一份修改的文件列表
// 如果返回 false,会进行全量编译,删除上一次的输出内容
return false
}
@Override
void transform(Context context, Collection<TransformInput> inputs,
Collection<TransformInput> referencedInputs,
TransformOutputProvider outputProvider, boolean isIncremental)
throws IOException, TransformException, InterruptedException {
println "========= start transform from $originName to $targetName ==========="
long startTime = System.currentTimeMillis()
// 需要添加进classPool的路径
def path = []
// Transform的inputs有两种类型,一种是项目内的目录,一种是第三方的jar包,要分开遍历
inputs.each { TransformInput input ->
//对类型为“directory” 的 input 进行遍历
input.directoryInputs.each { DirectoryInput directoryInput ->
println directoryInput.file.name
def origin = new File(directoryInput.file,originName.toString().replace('.', File.separator)+".class")
def target = new File(directoryInput.file,targetName.toString().replace('.', File.separator)+".class")
if (origin.exists()){
println "find origin file from dir input"
originFile = origin
path.add(directoryInput.file.absolutePath)
}
if(target.exists()){
println "find target file from dir input"
targetFile = target
path.add(directoryInput.file.absolutePath)
}
// 获取output目录
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
extractRoot = dest
// println "tatget:${targetFile},originClass:${originFile}"
FileUtils.copyDirectory(directoryInput.file, dest)
}
//对第三方的 jar 包文件,进行遍历
input.jarInputs.each { JarInput jarInput ->
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
//生成输出路径
def dest = outputProvider.getContentLocation(md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
// 将input的目录复制到output指定目录
FileUtils.copyFile(jarInput.file, dest)
if (dest.exists()) {
if (JarZipUtil.containsEntry(dest, targetName.replace('.', '/') + '.class')){
println "find target file from jar input"
targetFile = dest
path.add(dest.absolutePath)
}
if (JarZipUtil.containsEntry(dest, originName.replace('.', '/') + '.class')){
println "find origin file from jar input"
originFile = dest
path.add(dest.absolutePath)
}
}
}
}
println "ready to add path:${path.toString()}"
println "get origin file: $originFile"
println "get target file: $targetFile"
modifySuper(path,extractRoot.absolutePath)
println "=================Cost time : ${System.currentTimeMillis() - startTime} =============="
}
/**
* 替换父类
* @param workDir
* @param targetPath
* @return
*/
boolean modifySuper(List workDir, String targetPath){
ClassPool pool = ClassPool.getDefault()
def currentCP = []
//project.android.bootClasspath 加入android.jar,否则找不到android相关的所有类
project.android.bootClasspath.each {
currentCP += pool.appendClassPath(it.absolutePath)
}
workDir.each {
println "insert path $it"
currentCP += pool.appendClassPath(it)
}
CtClass c = pool.getCtClass(originName)
if (c.isFrozen()) {
c.defrost()
}
// 替换
pool.importPackage(targetName)
c.setSuperclass(pool.getCtClass(targetName))
// 重要 -- 将Application方法调用指向父类
//(如果不这样做 在android 6.0以及某些机型上会失效,本质上还是因为没有替换成功 但是高版本却兼容了这个问题)
c.getDeclaredMethods().each { CtMethod cm ->
cm.instrument(new ExprEditor() {
@Override
void edit(MethodCall m) throws CannotCompileException {
println "m.className:$m.className"
// $_代表的是方法的返回值 $$是所有方法参数的简写
if (m.className == 'android.app.Application' && m.methodName == cm.name) {
if (m.signature.endsWith("V"))
m.replace("{super.${m.methodName}(\$\$);}")
else
m.replace("{\$_ = super.${m.methodName}(\$\$);}")
}
}
})
}
c.writeFile(targetPath)
c.detach()
// 释放资源
currentCP.each {
pool.removeClassPath(it)
}
def tempDir = new File(originFile.getParent(),"temp")
def path = originFile.absolutePath
println "tempDir:${tempDir.absolutePath}"
JarZipUtil.unzipJar(path,tempDir.absolutePath)
if (tempDir.exists()){
def deleteClass = new File(tempDir,originName.toString().replace('.', File.separator)+".class")
if (deleteClass.exists()){
println "delete class result : ${deleteClass.delete()}"
println "delete origin jar: ${originFile.delete()}"
JarZipUtil.zipJar(tempDir.absolutePath,path)
}
FileUtils.deleteDirectory(tempDir)
}
println "success replace $originName super class as $targetName"
println "================ end replace application transform =========="
}
}
jar工具类
package com.hjl.plugin
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
class JarZipUtil {
/**
* 将该jar包解压到指定目录
* @param jarPath jar包的绝对路径
* @param destDirPath jar包解压后的保存路径
*/
static void unzipJar(String jarPath, String destDirPath) {
if (jarPath.endsWith('.jar')) {
JarFile jarFile = new JarFile(jarPath)
Enumeration<JarEntry> jarEntrys = jarFile.entries()
while (jarEntrys.hasMoreElements()) {
JarEntry jarEntry = jarEntrys.nextElement()
if (jarEntry.directory) {
continue
}
String entryName = jarEntry.getName()
String outFileName = destDirPath + "/" + entryName
File outFile = new File(outFileName)
outFile.getParentFile().mkdirs()
InputStream inputStream = jarFile.getInputStream(jarEntry)
FileOutputStream fileOutputStream = new FileOutputStream(outFile)
fileOutputStream << inputStream
fileOutputStream.close()
inputStream.close()
}
jarFile.close()
}
}
/**
* 重新打包jar
* @param packagePath 将这个目录下的所有文件打包成jar
* @param destPath 打包好的jar包的绝对路径
*/
static void zipJar(String packagePath, String destPath) {
File file = new File(packagePath)
JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(destPath))
file.eachFileRecurse { File f ->
String entryName = f.getAbsolutePath().substring(file.absolutePath.length() + 1)
outputStream.putNextEntry(new ZipEntry(entryName))
if(!f.directory) {
InputStream inputStream = new FileInputStream(f)
outputStream << inputStream
inputStream.close()
}
}
outputStream.close()
}
static boolean containsEntry(File f, String entry) {
ZipFile zf = new ZipFile(f)
boolean ret = (zf.getEntry(entry) != null)
zf.close()
return ret
}
}
在自定义插件注册transform
class CustomPlugin implements Plugin<Project>{
@Override
void apply(Project project) {
project.getExtensions().findByType(AppExtension.class)
.registerTransform(new ApplicationTransform(project))
....
}
}