Method not found

Now the infrastructure of a slightly larger app might look something like this:

After more projects, there may be dozens or even hundreds of these business storehouses. In most cases, these business storehouses are integrated into the final APP package in the form of AAR, and these dozens of business storehouses are developed by different teams respectively, and then gradually evolve into the following picture:

It is well known that when Android is packaged with different versions of an AAR, the default is to always refer to the version with the highest version number. Here’s a problem:

Library based on iterative upgrade Changes are likely to be in some ways, such as modifying method return value, the parameters of the modified method, and even to delete method, etc., but if you touch the above scenario will be careful, because a lot of business warehouse dependent or older versions of basic library, their operation is normal, The method not found error will be reported if the base library version is updated to remove a method, assuming they use that method again.

Someone asked why don’t you just force the warehouse to upgrade every time you upgrade the base library? Of course not… Because many businesses are cross-functional, they won’t want to follow you every time you upgrade for no good reason. Crash (method not found) crash (method not found) crash (method not found)

How to solve this problem?

The way to solve the problem is to get information about the class’s methods at compile time, and try to figure out if there are any methods that don’t exist in a particular line. The obvious thing to do first is to get all the classes. A transform input will give you all the classes you want. Note that if you use a transform input, you must have an output. Otherwise your app will run with a class Not found error.

Once we have these classes in hand, we can use the Javassit tool to analyze our class and raise an exception with an empty ExprEditor. An exception means that there is no class in the classpool or that there is a class but there is no method.

It is important to include android.jar when building the Classpool for Javassit, otherwise many android system methods will not be found. The different projects here use different versions of the Android SDK. So one small feature we need to implement is to dynamically get the path to Android.jar.

Ok, the idea and key points of implementing the plug-in are elaborated and then directly on the code

Code implementation

Dynamically obtain the path of android.jar under project (thanks to didi-Booster’s concise implementation, which saved me a lot of trouble);

import java.io.File import java.io.FileNotFoundException import java.util.Properties private val HOME = System.getproperty ("user.home") private val CWD = System.getProperty("user.dir") /** * * * Because different people, different operating systems, different projects have different android.jar paths * * We need to get this path and add it to the classPath before we can do related operations on bytecode otherwise ASM and Javassist ** */ class AndroidSdk {companion Object {/** ** You can get the path of your android.jar using apiLevel ** @param apiLevel  * @return */ fun getAndroidJar(apiLevel: Int = findPlatform()): File { val jar = File(getLocation(), "platforms${File.separator}android-${apiLevel}${File.separator}android.jar") return jar.takeIf { it.exists() } ? : throw FileNotFoundException(jar.path) } fun findPlatform(): Int = File(getLocation(), "platforms").listFiles()? .filter { it.name.startsWith("android-") && File(it, "android.jar").exists() }?.map { it.name.substringAfter("android-") }?.max()?.toInt() ?: Throw RuntimeException("No platform found") /** * ANDROID_HOME environment variable * 2. Android Command in PATH * 3. local.properties * 4. platform dependent path: * * - macosx: ~/Library/Android/sdk * - linux: ~/Android/sdk * - windows: ~\AppData\Local\Android\sdk */ fun getLocation(): File = System.getenv("ANDROID_HOME")?.takeIf { it.isNotBlank() }?.let { File(it) }?.takeIf { it.exists() && it.isDirectory } ?: System.getenv("PATH").splitToSequence(File.pathSeparator).map { File(it, "android") }.find { it.exists() && it.canExecute() }?.canonicalFile?.parentFile?.parentFile ?: File(CWD, "local.properties").let { local -> if (local.exists()) { val props = Properties(); local.inputStream().use { props.load(it) } props.getProperty("sdk.dir", null)? .let { File(it) }? .takeIf { it.exists() && it.isDirectory } } else { null } } ? : when { OS.isMac() -> File(HOME, "Library${File.separator}Android${File.separator}sdk").takeIf { it.exists() && it.isDirectory } OS.isLinux() -> File(HOME, "Android${File.separator}sdk").takeIf { it.exists() && it.isDirectory } OS.isWindows() -> File(HOME, "AppData${File.separator}Local${File.separator}Android${File.separator}sdk").takeIf { it.exists() && it.isDirectory } else -> null } ? : throw RuntimeException("`ANDROID_HOME` is not set and `android` command not in your PATH") } }Copy the code

Let’s look at the key transform:

import com.android.build.api.transform.Format import com.android.build.api.transform.QualifiedContent import com.android.build.api.transform.Transform import com.android.build.api.transform.TransformInvocation import com.android.build.gradle.internal.pipeline.TransformManager import javassist.ClassPool import javassist.expr.ExprEditor import javassist.expr.MethodCall import org.gradle.api.Project import java.io.File import java.util.zip.ZipFile class MethodNotFoundTransform(project: Project) : Transform() { val project = project override fun getName(): String { return "MethodNotFoundTransform" } override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> { return TransformManager.CONTENT_CLASS } override fun isIncremental(): Boolean { return false } override fun getScopes(): MutableSet<in QualifiedContent.Scope> { return TransformManager.SCOPE_FULL_PROJECT } override fun transform(transformInvocation: TransformInvocation) { val outputProvider = transformInvocation.outputProvider val classPool = ClassPool() val androidSdkPath = AndroidSdk.getAndroidJar(ProjectFileRead.getCompileSdkVersion(project)).absolutePath Println (" -- -- -- -- -- -- -- -- -- -- -- -- -- androidSdkPath: $androidSdkPath ") / / here must will compile time using android. The jar is added to the path Otherwise there will be a lot of system approach can't find it Thus the situation of the false positives classPool. AppendClassPath (androidSdkPath) Val errorInfoPath = JenkisHelper. GetJenkinsFindDir (project) + File. The separator + "MethodDetect. TXT" println (" method will not Found information is written to :" + errorInfoPath) val errorInfoFile = File(errorInfoPath) var errorInfoMarkString = "" val destJarList = ArrayList < String > () / / handles all class of input transformInvocation inputs. ForEach {input - > / / processing jars input. JarInputs. ForEach { // If there is an input, there must be an output. Otherwise it will go wrong Lead to many class lost val dest = outputProvider. GetContentLocation (jarInput. File. AbsolutePath, jarInput. ContentTypes, Jarintes.scopes, format.jar) // The process of copying must not lose jarintes.file.copyto (dest, True) / / copies we finished the class Path also add to the classPool classPool. AppendClassPath (dest. AbsolutePath) / / copy each time will be output to a list record location Destjarlist.add (dest. AbsolutePath)} destjarlist.add (dest. AbsolutePath)} destjarlist.add (dest. AbsolutePath)} destjarlist.add (dest. AbsolutePath)} destjarlist.add (dest. Temporary also add in the input. DirectoryInputs. ForEach {classPool. AppendClassPath (it) file) absolutePath) val dest = outputProvider.getContentLocation( it.name, it.contentTypes, it.scopes, Format.DIRECTORY) println("name:" + it. Name + "dest" + dest it.file.copyRecursively(dest, DestJarList. ForEach {jar -> val zipFile = zipFile (jar) destJarList. Zipfile.entries ().assequence ().filter {// We only deal with class files, Because some jar packages may carry other files it.name.endswith ("class")}. ForEach {zipEntry -> Val t1 = zipentry.name.replace ("/", ".") // t1 takes the value xxxx.class we can remove the. Class suffix completely to get our full class name val t2 = t1. T1.lastindexof (".")) val t3 = classPool. GetCtClass (t2) // Traverse every method in each class t3.methods.foreach { CtMethod -> // Not every method needs to be checked, filter out system methods that we do not need to deal with, The third party SDK method and so on Just check if our own business logic code (ctMethod. DeclaringClass. Name. StartsWith (" com. Xiaomi. Space ") &&! ctMethod.declaringClass.name.startsWith("com.xiaomi.analytics")) { ctMethod.instrument(object : ExprEditor() { override fun edit(m: MethodCall?) { super.edit(m) try { m?.method?.instrument(ExprEditor()) } catch (e: Exception) {e.message?. Let {errorInfoMarkString += "${e.message}\n" errorInfoMarkString += "The problem may occur in class: ${ctMethod. DeclaringClass. Name} ${ctMethod. Name} method \ n "errorInfoMarkString + = "---------------------------------------------\n" //} } } } }) } } errorInfoFile.writeText(errorInfoMarkString) } } println("---------------MethodNotFoundTransform transform end !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" )}}Copy the code

Finally, the detection results will be output to CI

This way you can print out the method Not Found information every time you compile your project and never worry about this type of exception.