We’ve been using Retroift all our lives and know that its core is dynamic proxy. For example, in a previous article reviewing Retrofit’s source code, the coroutine implementation also briefly mentions dynamic proxies (to fill holes dug earlier…). .

Ahem, instead of focusing on the cause, let’s get back to the present.

This time is mainly to analyze the role of dynamic proxy and implementation principle. Now that you’ve analyzed the principles, it’s natural to finally start a simple Demo modeled after Retrofit.

Through the final Demo implementation, I believe that dynamic proxy you also basically no problem.

Static agent

Since speaking of dynamic proxy, naturally not static proxy. So what exactly is a static proxy? Let’s look at a simple scenario.

Suppose there is a Bird interface to represent some Bird characteristic, such as fly flight characteristic

interface Bird {
    fun fly()
}
Copy the code

Now there are sparrow, eagle and other animals, because they are birds, so they all implement the Bird interface, internal implementation of their own fly logic.

// Sparrow: Bird {override fun fly() {println("Sparrow: Thread. Sleep (1000)}} override fun fly() {override fun fly() {println("Eagle: is fly.") Thread.sleep(2000) } }Copy the code

The flying ability of sparrows and eagles has been realized. Now there is a requirement: we need to count the flying time of sparrows and eagles respectively.

What would you do? I believe that when we just learn programming, we will think of: this is not easy to directly in the fly method of sparrow and eagle respectively statistics can be.

This implementation will not be a problem if there are not many species of birds implemented, but once there are many species of birds implemented, there will be a lot of logic to repeat this method because we need to add statistical time logic to the FLY method for each species of bird.

Therefore, in order to solve this kind of meaningless repetitive logic, we can use a ProxyBird to realize the statistics of duration proxy.

class BirdProxy(private val bird: Bird) : Bird {
    override fun fly() {
        println("BirdProxy: fly start.")
        val start = System.currentTimeMillis() / 1000
        bird.fly()
        println("BirdProxy: fly end and cost time => ${System.currentTimeMillis() / 1000 - start}s")
    }
}
Copy the code

ProxyBird implements the Bird interface and accepts external objects that implement the Bird interface. When the fly method of ProxyBird is called, the fly method of the passed object is indirectly called, and the time length statistics are also carried out.

class Main {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            ProxyBird(Sparrow()).fly()
            println()
            ProxyBird(Eagle()).fly()
        }

    }
}
Copy the code

The final output is as follows:

ProxyBird: fly start.
Sparrow: is fly.
ProxyBird: fly end and cost time => 1s
 
ProxyBird: fly start.
Eagle: is fly.
ProxyBird: fly end and cost time => 2s
Copy the code

The pattern above is static proxy, and many readers may already be using this method without realizing it is static proxy.

So what are the benefits?

From the above examples, it is natural to realize that static proxies mainly help us solve the following problems:

  1. Reduce the repetition of logic writing, provide a unified and convenient processing entry.
  2. Encapsulate the implementation details.

A dynamic proxy

Why have a dynamic proxy when you already have a static proxy?

Everything comes into being for its own sake, to solve problems that the former cannot solve.

So dynamic proxy is to solve the problem that static proxy can not solve, or its disadvantages.

Suppose we now add a new feature to Bird: chirp.

So what needs to change based on the previous static proxy?

  1. Modify theBirdInterface, addedchirpMethods.
  2. Modified respectivelySparrowwithEagleAnd add new ones to themchirpThe concrete implementation of.
  3. Modify theProxyBirdTo realizechirpProxy methods.

1 and 3 are ok, especially 2. Once there are many kinds of birds to implement the Bird interface, it will be very complicated, and then it is really a whole body.

This is still a change to the existing Bird interface, but you may need to add another interface, such as Fish, to implement fish-related features.

A new agent, ProxyFish, is generated to manage the agents for the fish.

So from this point of view, we can see that static proxy is very poor mobility, and for those functions that do not change much after implementation, it can be considered to implement it, which is exactly the static nature of its name.

So can dynamic proxy solve this situation? Don’t worry, we’ll see if we can solve it.

Next, we add the chirp method for Bird

interface Bird {
    fun fly()
    
    fun chirp()
}
Copy the code

This interface is then implemented through dynamic proxies

class Main {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            val proxy = (Proxy.newProxyInstance(this::class.java.classLoader, arrayOf(Bird::class.java), InvocationHandler { proxy, method, args ->
                if (method.name == "fly") {
                    println("calling fly.")
                } else if (method.name == "chirp") {
                    println("calling chirp.")
                }
            }) as Bird)
            
            proxy.fly()
            proxy.chirp()
        }
    }
}
Copy the code

The output is as follows:

calling fly.
calling chirp.
Copy the code

NewProxyInstance static method to create a Proxy that implements the Bird interface. The method mainly has three parameters:

  1. ClassLoader: a ClassLoader that generates proxy classes.
  2. Interface Interface Class array: indicates the corresponding interface Class.
  3. InvocationHandler: InvocationHandler object, callbacks to all proxy methods.

The key point here is the third argument, all proxy methods that call the proxy class are called back in the InvocationHandler object via its Invoke method

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}
Copy the code

This is why the logic that determines the invocation of a specific interface method is written above in the Invoke method of the InvocationHandler object.

So how does it work? Why is it a proxy class? I don’t see where the proxy class is either. How come all calls go through InvocationHandler?

These questions are normal, and they are common when you start working with dynamic proxies. The direct cause of these questions is that we can’t see the so-called proxy classes directly. Because dynamic proxies generate proxy classes at run time, the source code is not directly visible as it is at compile time.

Then the goal is clear: solve the problem of not seeing the source code.

Since it is generated at run time, why not write the generated proxy classes to the local directory at run time? ProxyGenerator is provided for how to write a Proxy. Its generateProxyClass method helps us get the generated proxy class.

class Main {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            val byte = ProxyGenerator.generateProxyClass("\$Proxy0", arrayOf(Bird::class.java))
            FileOutputStream("/Users/{path}/Downloads/\$Proxy0.class").apply {
                write(byte)
                flush()
                close()
            }
        }
    }
}
Copy the code

Run the code above to find the $proxy0.class file in the Downloads directory, drag it directly to the compiler, and open it as follows:

public final class $Proxy0 extends Proxy implements Bird { private static Method m1; private static Method m4; private static Method m2; private static Method m3; private static Method m0; public $Proxy0(InvocationHandler var1) throws { super(var1); } public final boolean equals(Object var1) throws { try { return (Boolean)super.h.invoke(this, m1, new Object[]{var1}); } catch (RuntimeException | Error var3) { throw var3; } catch (Throwable var4) { throw new UndeclaredThrowableException(var4); } } public final void fly() throws { try { super.h.invoke(this, m4, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final String toString() throws { try { return (String)super.h.invoke(this, m2, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final void chirp() throws { try { super.h.invoke(this, m3, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final int hashCode() throws { try { return (Integer)super.h.invoke(this, m0, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } static { try { m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object")); m4 = Class.forName("com.daily.algothrim.Bird").getMethod("fly"); m2 = Class.forName("java.lang.Object").getMethod("toString"); m3 = Class.forName("com.daily.algothrim.Bird").getMethod("chirp"); m0 = Class.forName("java.lang.Object").getMethod("hashCode"); } catch (NoSuchMethodException var2) { throw new NoSuchMethodError(var2.getMessage()); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError(var3.getMessage()); }}}Copy the code

First, $Proxy0 inherits Proxy and implements the familiar Bird interface. It then takes a var1 parameter in its constructor of type InvocationHandler. Moving on to methods, we implemented the default equals, toString, and hashCode methods of the class, and found the fly and chirp methods we needed.

The fly method, for example, is called

super.h.invoke(this, m4, (Object[])null)
Copy the code

The h here is var1, the InvocationHandler object.

At this point, the fog is clear. Call the Invoke method and pass in the proxy class’s own This, corresponding method information, and method parameters.

So we only need to do the logic that handles the different proxy methods in the invoke method of the last parameter to the dynamic proxy, InvocationHandler. The nice thing about this is that no matter how you add and remove interface methods in Bird, I can just tweak the Invoke processing logic to minimize the scope of change.

This is one of the benefits of dynamic proxies (the other major benefit is, of course, less proxy class writing).

The best example of dynamic proxies in Android is Retrofit. As a network framework, an App’s interface to network requests naturally increases with App iterations. For this frequently changing situation, Retrofit uses dynamic proxies as an entry point to expose a corresponding Service interface in which the associated interface request methods are defined. So every time we add a new interface, we don’t need to make too many other changes, and the relevant network request logic is encapsulated in the dynamic proxy invoke method. Of course, Retrofit is based on adding Annomation annotations to parse the different network request mode and related parameter logic. Finally, the parsed data is encapsulated and passed to the underlying OKHttp.

So the core of Retrofit is dynamic proxy and annotation parsing.

The principle analysis part of this article is completed. Finally, since the relationship between dynamic proxy and Retrofit is analyzed, I provide a Demo to consolidate dynamic proxy and use some ideas from Retroift to encapsulate a simple version of the dotting system.

Demo

Demo is a simple simulation system, by defining the Statistic class to create dynamic proxy, exposed Service interface, as follows:

class Statistic private constructor() {
 
    companion object {
        @JvmStatic
        val instance by lazy { Statistic() }
    }
 
    @Suppress("UNCHECKED_CAST")
    fun <T> create(service: Class<T>): T {
        return Proxy.newProxyInstance(service.classLoader, arrayOf(service)) { proxy, method, args ->
            return@newProxyInstance LoadService(method).invoke(args)
        } as T
    }

}
Copy the code

Create the corresponding dynamic proxy class by importing the Service interface, and then encapsulate the logical handling of method calls in the Service interface into the LoadService invoke method. Of course Statistic also uses annotations to parse different types of events.

For example, we need to do click and display statistics for Button and Text respectively.

First, we can define the corresponding Service interface as follows, which is named StatisticService

interface StatisticService { @Scan(ProxyActivity.PAGE_NAME) fun buttonScan(@Content(StatisticTrack.Parameter.NAME) name:  String) @Click(ProxyActivity.PAGE_NAME) fun buttonClick(@Content(StatisticTrack.Parameter.NAME) name: String, @Content(StatisticTrack.Parameter.TIME) clickTime: Long) @Scan(ProxyActivity.PAGE_NAME) fun textScan(@Content(StatisticTrack.Parameter.NAME) name: String) @Click(ProxyActivity.PAGE_NAME) fun textClick(@Content(StatisticTrack.Parameter.NAME) name: String, @Content(StatisticTrack.Parameter.TIME) clickTime: Long) }Copy the code

The proxy class object of the dynamic proxy is obtained through Statistic

private val mStatisticService = Statistic.instance.create(StatisticService::class.java)
Copy the code

With the corresponding proxy class object, all that is left is to call directly from the corresponding location.

class ProxyActivity : AppCompatActivity() { private val mStatisticService = Statistic.instance.create(StatisticService::class.java) companion object { private const val BUTTON = "statistic_button" private const val TEXT = "statistic_text" const val PAGE_NAME = "ProxyActivity" } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val extraData = getExtraData() setContentView(extraData.layoutId) title = extraData.title // statistic scan mStatisticService.buttonScan(BUTTON) mStatisticService.textScan(TEXT) } private fun getExtraData(): MainModel = intent? .extras? .getParcelable(ActivityUtils.EXTRA_DATA) ? : throw NullPointerException("intent or extras is null") fun onClick(view: View) { // statistic click if (view.id == R.id.button) { mStatisticService.buttonClick(BUTTON, System.currentTimeMillis() / 1000) } else if (view.id == R.id.text) { mStatisticService.textClick(TEXT, System.currentTimeMillis() / 1000) } } }Copy the code

Such a simple dotting of the upper logical packaging is complete. Due to lack of space The internal implementation logic is not developed.

The relevant source code is in the Android-API-Analysis project, interested can check.

Switch the branch to Feat_proxy_dev before using it

project

Android_startup: provides a simple and efficient way to initialize components during application startup, optimizing startup speed. Not only does it support all the features of Jetpack App Startup, but it also provides additional synchronous and asynchronous waiting, thread control, and multi-process support.

AwesomeGithub: Based on Github client, pure exercise project, support componentized development, support account password and authentication login. Kotlin language for development, the project architecture is based on Jetpack&DataBinding MVVM; Popular open source technologies such as Arouter, Retrofit, Coroutine, Glide, Dagger and Hilt are used in the project.

Flutter_github: a cross-platform Github client based on Flutter, corresponding to AwesomeGithub.

Android-api-analysis: A comprehensive analysis of Knowledge points related to Android with detailed Demo to help readers quickly grasp and understand the main points explained.

Daily_algorithm: an algorithm of the day, from shallow to deep, welcome to join us.