background

When I took over a project, I encountered the logic of switching the main thread. The code flow of the original project was as follows:

  1. xxPresenterAn observer will be created for the binary librarySDKService(usually a callback in child threads), denoted byinnerObserver
  2. xxActivityYou also need to create an observer for the main thread callback, denoted asuiObserver
  3. xxPresenterAfter receiving the innerObserver callback, a thread switch is made through the main thread handler, which ultimately fires the corresponding uiObserver method
  4. The business requirements callback method is all therexxActivityTo perform subsequent operations on the main thread,innerObserverUsed almost exclusively for thread switching

Existing problems

  1. In step 2/3 of the figure, for an observer of the same type, you need to implement both activity and Presenter, which generate a lot of template code
  2. Step 6 in figure, receivedSDKServiceAfter a callback, Presenter needs to build Message and set the various callback arguments, which is completely manually configured by the developer and is inefficient, error-prone and inflexible
  3. In step 11, when switching through the handler thread, each argument needs to be restored from message in turn, which again depends on the developer to handle manually
  4. When the Observer changes (for example, the order or type of the parameter list changes), prenter and Handler must be updated simultaneously
  5. At the peak of our project, a SDKService had nearly 100 observers to be set up, and some of the methods in the observer even exceeded 45. As a result, creating a blank anonymous inner class of the Observer in a Presenter would have more than 100 lines of code and too much template code
  6. .

Transformation ideas

According to known conditions:

  1. All observers are interfacesinterfacetype
  2. Presenter implementationinnerObserverIt is used only for thread switching that eventually triggers the creation of an observer at the UI layer -> i.e., there is unified logic for enhancing functionality

Naturally associated with the dynamic proxy in proxy mode:

  1. Create a ThreadSwitcher helper Class that automatically generates a dynamic proxy object (innerObserver) based on the type Class of the observer passed in. This step can save a lot of template code generated in Prsetner due to new Observer (){}, and does not need to modify the code when the observer interface changes, automatically complete adaptation, pseudo-code is as follows: Observer innerOb = ThreadSwitcher.generateInnerObserver(Observer.class)

  2. The ThreadSwitcher Class also exposes the interface for the UI layer to pass in an observer for the main thread, which is cached in Map

    for subsequent mainthread switching
    ,iobserver>

  3. When the underlying SDK calls back to a dynamic proxy object, the InvocationHandler#invoke method is finally triggered. The method signature is as follows. We simply construct a runnable in the body of the method and post it to the main thread as needed:

// package java.lang.reflect.InvocationHandler.java
/ * * *@paramMethod Specifies the callback method that is triggered in the interface@paramArgs method argument list */
public Object invoke(Object proxy, Method method, Object[] args);
Copy the code
  1. The runnable is constructed by looking up the OBSERVER injected by the UI layer and firing the corresponding method, which is already told in the InvocationHandlermethodAnd in fact the cordsargs, so it can be directly throughmethod.invoke(uiObserver,args)To trigger the corresponding method of uiObserver, see the code in the next section

Use of dynamic proxies

import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy

object ThreadSwitcher {
    // The observer injected by the UI layer is called back in the main thread
    val uiObserverMap = mutableMapOf<Class<*>, Any>()
    val targetHandler: Handler = Handler(Looper.mainLooper())

    private fun runOnUIThread(runnable: Runnable) {
        // Create a mainLooper handler and post Runnable
    }

    // Generate the proxy class
    fun <O> generateInnerObserver(clz: Class<O>): O? {
        // Fixed notation, passing in a classLoader and a list of interfaces to implement, as well as the implementation of the core InvocationHandler, within which enhancements are made
        return Proxy.newProxyInstance(clz.classLoader, arrayOf(clz), object : InvocationHandler {
            override fun invoke(proxy: Any? , method:Method? , args:Array<out Any>?: Any? {

                // 1. Construct runnable for main thread switching
                val runnable = Runnable {
                    
                    // 3. Find uiObserveruiObserverMap[clz]? .let { uiObserver ->valresult = method? .invoke(uiObserver, args) result } }// 2. Throw runnable to main thread
                runOnUIThread(runnable)

                // 4. Method returns a value based on the actual type. Void returns null
                return null}})as O  // The interface type to be implemented on demand}}Copy the code

For specific encapsulation implementation, please refer to the following links:

  • Making the warehouse
  • The implementation class ThreadSwitcher. Kt
  • The test class ThreadSwitcherTest. Kt

The process after transformation is as follows:

Source code analysis

Dynamic proxy implementation is very simple, two or three lines of code can be done, the system must do a lot of packaging, the dirty work to do, let’s take a look

Start with the entry method: java.lang.reflect.proxy #newProxyInstance

/ / package Java. Lang. Reflect. Proxy. Based on Java API 29
private static finalClass<? >[] constructorParams = { InvocationHandler.class };public static Object newProxyInstance(ClassLoader loader, Class
       [] interfaces, InvocationHandler h){
    finalClass<? >[] intfs = interfaces.clone();// Find the generated class type in the cache, or generate it if it doesn't existClass<? > cl = getProxyClass0(loader, intfs);// Reflection calls the constructor Proxy(InvocationHandler) to create and return the instance
    finalConstructor<? > cons = cl.getConstructor(constructorParams);final InvocationHandler ih = h;
    if(! Modifier.isPublic(cl.getModifiers())) { cons.setAccessible(true);
    }
    return cons.newInstance(new Object[]{h});
}

private static finalWeakCache<ClassLoader, Class<? >[], Class<? >> proxyClassCache =new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

/** * Create proxy class */
private staticClass<? > getProxyClass0(ClassLoader loader,Class<? >... interfaces) {// Limit the number of interface methods
    if (interfaces.length > 65535) { throw new IllegalArgumentException("interface limit exceeded"); }

    // Get the created proxy class from the cache first, or create it if it does not exist
    return proxyClassCache.get(loader, interfaces);
}
Copy the code

The key proxyClassCache is a second-level cache class (WeakCache). The final implementation class is obtained by calling its GET method, and its constructor signature is as follows:

// java.lang.reflect.WeakCache.java

/**
    * Construct an instance of {@code WeakCache}
    *
    * @param subKeyFactory a function mapping a pair of
    *                      {@code (key, parameter) -> sub-key}
    * @param valueFactory  a function mapping a pair of
    *                      {@code (key, parameter) -> value}
    * @throws NullPointerException if {@code subKeyFactory} or
    *                              {@code valueFactory} is null.
    */
public WeakCache(BiFunction
       
         subKeyFactory, BiFunction
        
          valueFactory)
        ,>
       ,> {
Copy the code

As you can guess from the name of the parameter, it is generated by valueFactory. Let’s go back to the Proxy class:

// java.lang.reflect.Proxy.java

private static finalWeakCache<ClassLoader, Class<? >[], Class<? >> proxyClassCache =new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

/** * A factory function that generates, defines and returns the proxy class given * the ClassLoader and array of interfaces. */
private static final class ProxyClassFactory
    implements BiFunction<ClassLoader.Class<? > [].Class<? >>{
    // The prefix of all dynamic proxy class names
    private static final String proxyClassNamePrefix = "$Proxy";

    // The unique number in the class name of each dynamic Proxy class is: $Proxy+ number
    private static final AtomicLong nextUniqueNumber = new AtomicLong();

    @Override
    publicClass<? > apply(ClassLoader loader, Class<? >[] interfaces) {// Omit some code: do some validation on the array of interfaces passed in

        String proxyPkg = null; // Finally implement the package path where the class resides
        int accessFlags = Modifier.PUBLIC | Modifier.FINAL; // The default access for the generated proxy class is public final

        // Check the interface array: If the interface to be implemented is non-public, the final implementation of the proxy class is also non-public, and the non-public interfaces must be in the same package
        for(Class<? > intf : interfaces) {int flags = intf.getModifiers();
            if(! Modifier.isPublic(flags)) { accessFlags = Modifier.FINAL; String name = intf.getName();int n = name.lastIndexOf('. ');
                String pkg = ((n == -1)?"" : name.substring(0, n + 1));
                if (proxyPkg == null) {
                    proxyPkg = pkg;
                } else if(! pkg.equals(proxyPkg)) {throw new IllegalArgumentException(
                        "non-public interfaces from different packages"); }}}// If the interfaces to be implemented are all public, the default package path is used
        if (proxyPkg == null) { proxyPkg = ""; }

        {
            List<Method> methods = getMethods(interfaces); // Get the methods of all interfaces recursively (including their parent) and manually add equals/hashCode/toString
            Collections.sort(methods, ORDER_BY_SIGNATURE_AND_SUBTYPE); // Sort all interface methods
            validateReturnTypes(methods); // verify interface methods: ensure that methods with the same name return the same typeList<Class<? >[]> exceptions = deduplicateAndGetExceptions(methods);// Remove duplicate methods and get outlier information for each method

            Method[] methodsArray = methods.toArray(newMethod[methods.size()]); Class<? >[][] exceptionsArray = exceptions.toArray(newClass<? >[exceptions.size()][]);long num = nextUniqueNumber.getAndIncrement(); // Generates digital information about the current proxy implementation class
            String proxyName = proxyPkg + proxyClassNamePrefix + num; // Concatenate to generate the Proxy class name, default: $Proxy+ number

            return generateProxy(proxyName, interfaces, loader, methodsArray, exceptionsArray); // Generate the proxy Class using native methods}}@FastNative
    private static nativeClass<? > generateProxy(String name, Class<? >[] interfaces, ClassLoader loader, Method[] methods, Class<? >[][] exceptions);/** * get all interface methods based on the incoming interface class information and add three additional equals/hashCode/toString methods */
    private static List<Method> getMethods(Class
       [] interfaces) {
        List<Method> result = new ArrayList<Method>();
        try {
            result.add(Object.class.getMethod("equals", Object.class));
            result.add(Object.class.getMethod("hashCode", EmptyArray.CLASS));
            result.add(Object.class.getMethod("toString", EmptyArray.CLASS));
        } catch (NoSuchMethodException e) {
            throw new AssertionError();
        }

        getMethodsRecursive(interfaces, result); // Get all the methods of the interface at once by recursive reflection
        returnresult; }}Copy the code

What does a dynamically generated class look like?

Above we briefly analyzed the source code of dynamic proxy, we can know/infer the following information:

  1. The generated proxy class is called$ProxyNAmong themNIs a number that increases as the proxy class increases
  2. $ProxyNAll interface methods are implemented and added automaticallyequals/hashCode/toStringThree methods, so: –> a. Dynamic proxy generated classes should be able to be strongly converted to any incoming interface type –> B. The addition of three additional methods usually affects object comparison and requires manual assignment to distinguish
  3. Methods that trigger dynamic proxy classes eventually call backInvocationHandler#invokeMethod, andInvocationHandlerIs through theProxy#newProxyInstanceIncoming, therefore: –> guess generated$ProxyNClasses should be inherited fromProxy

It’s best to export the generated $ProxyN and see the actual code:

  1. What is found online is usually provided using the JVMsun.misc.ProxyGeneratorClass, but this class does not exist in Android, manual copy of the corresponding JAR package into Android use is also problematic
  2. Try using the bytecode manipulation library orClass#getResourceAsStreamAfter all, tools on the JVM cannot be used directly on the Android VIRTUAL machine
  3. Finally, the next best thing is to get it by reflection$ProxyNClass structure, as for method calls throughInvocationHandler#invokeMethod to print the stack to view
// 1. Customize an interface as follows
package org.lynxz.utils.observer
interface ICallback {
    fun onCallback(a: Int, b: Boolean, c: String?).
}

// 2. Get the class structure by reflection
package org.lynxz.utils.reflect.ReflectUtilTest
@Test
fun oriProxyTest(a) {
    val proxyObj = Proxy.newProxyInstance(
        javaClass.classLoader,
        arrayOf(ICallback::class.java)
    ) { proxy, method, args -> // InvocationHandler#invoke method body
        RuntimeException("===> call stack:${method? .name}").printStackTrace() // 3. Print call stack informationargs? .forEachIndexed { index, any ->// 4. Print method arguments
            LoggerUtil.w(TAG, "===> method parameter:$index - $any") } ReflectUtil.generateDefaultTypeValue(method!! .returnType)// Generate the corresponding data according to the method return type
    }

    // ProxyGeneratorImpl is a custom implementation class that obtains class structure by reflection
    LoggerUtil.w(TAG, "===> Class structure :\n${ProxyGeneratorImpl(proxyObj.javaClass).generate()}")
    if (proxyObj is ICallback) { // The strongly generated dynamic proxy class is a custom interface
        proxyObj.onCallback(1.true."hello") // Trigger the interface method to trigger the InvocationHandler#invoke method to print the stack}}Copy the code

Finally, the log is as follows, which verifies the previous guess:

// ===>
public final class $Proxy6 extends java.lang.reflect.Proxy implements ICallback{
    public static final Class[] NFC;
    public static final Class[][] NFD;
    public$Proxy6(Class){... }public final boolean equals(Object){... }// The content of the method body is unknown
    public final int hashCode(a){... }public final String toString(a){... }public final void onCallback(int.boolean,String){...}
}

// Call stack:= = = > call stack: onCallback at org. Lynxz. Utils. Reflect. ReflectUtilTest $$proxyObj oriProxyTest $1.invok(ReflectUtilTest.kt:86) RuntimeException("===> call stack :${method? .name}").printStackTrace()
at java.lang.reflect.Proxy.invoke(Proxy.java:913) // invoke Proxy#invoke, which directly invokes InvocationHandler#invoke
at $Proxy6.onCallback(Unknown Source) ProxyObj. OnCallback (1, true, "hello")

// Print method real parameter data, serial number - value, same as we passed in===> method parameters:0 - 1===> method parameters:1 - true===> method parameters:2 - hello
Copy the code

Invoke Proxy#invoke source code, simply trigger InvocationHandler#invoke

// java.lang.reflect.Proxy.java
protected InvocationHandler h;
protected Proxy(InvocationHandler h) {
    Objects.requireNonNull(h);
    this.h = h;
}

// Trigger the invocationHandler method directly
// The InvocationHandler is passed in via Proxy#newProxyInstance and is eventually passed to the $Proxy6 constructor
private static Object invoke(Proxy proxy, Method method, Object[] args) throws Throwable {
    InvocationHandler h = proxy.h;  $Proxy6 = '$Proxy6'
    return h.invoke(proxy, method, args);
}
Copy the code

summary

  1. The dynamic proxy described above is based on the interface form of proxy, which is provided by the JVM built-in mechanism, and also supported by Android. As for the common class dynamic proxy, it needs to use bytecode manipulation library and other libraries to support
  2. For scenarios where implementation is not a concern, dynamic proxies can be used to generate interface implementation class objects directly
  3. Dynamic proxy implementation can also be used for scenarios requiring functionality enhancement, focusing onInvocationHandlerThe can
  4. The generated dynamic proxy class is cached and not created every time. See other articles for performance comparisons
  5. Related source code:
    • Making library
    • Thread-switching implementation class ThreadSwitcher.kt
    • Class threadSwitchertest.kt
    • Reflection gets class structure ProxyGeneratorImpl. Kt