Dynamically create ACTIVITY schema

Remember that we talked about the two problems of starting the Activity in the plug-in APK in the proxy Activity mode. Since the Activity in the plug-in is not registered in the manifest of the main project, it cannot go through a series of initialization processes at the system Framework level, which eventually leads to The obtained Activity instance has no life cycle and cannot use res resources.

Using a proxy activity can solve both problems, but there are some limitations:
  • The actual running Activity instances are actually ProxyActivity, not the Activity you really want to start;
  • ProxyActivity can only specify one LaunchMode, so the Activity in the plugin cannot customize the LaunchMode;
  • Statically registered BroadcastReceiver is not supported;
  • Often not all APKs can be loaded as plug-ins, plug-in projects need to rely on specific frameworks, and they need to follow certain "development specifications";

Especially the last one, it is impossible to directly use a normal APK as a plugin. This is actually not a limitation. If we need to develop plug-ins, we always hope that we can restrict and standardize the behavior of plug-ins through some frameworks. Before loading the plug-in, we can know the approximate functions of the plug-in, which not only facilitates the control of plug-in behavior. , but also to some extent secure plugins (running a completely unknown executable will never know what it will do). However, doing so requires that the plug-in must depend on a specific framework, which is an intrusive development for plug-ins, that is, the development of plug-ins cannot be as free as developing ordinary APPs.

So is there a way to circumvent these limitations and achieve completely non-intrusive development? For example, by dynamically loading the framework, you can directly run the APK installation package of "Flappy Bird" without installing it. This sounds like something that can only be done with ROOT permissions, or else just write an empty shell APK and load someone else's game installation package and it will run directly. However, someone did do it, by dynamically generating the Activity class.

The Activity that dynamically creates

the Activity mode plug-in is not a standard Activity object to have the above restrictions. Making it a standard Activity is the key to solving the problem. To make it a standard Activity, you need to register these Activities in the main project. . It is impossible to register all the activities of the plug-in APK in the host project in advance. Thinking that the proxy mode needs to register a proxy ProxyActivity, can you register a general Activity (such as TargetActivity) in the main project for all the activities in the plug-in. Woolen cloth? The solution is to dynamically create a TargetActivity when you need to start an Activity of the plug-in (such as PlugActivity), and the newly created TargetActivity will inherit all the common behaviors of PlugActivity, and the package name and class name of this TargetActivity are just registered with us in advance. If the TargetActivity is the same, we can start this Activity in a standard way.

The idea of ​​dynamically creating and compiling an Activity class at runtime is not a fantasy. The tools for dynamically creating classes include dexmaker and asmdex, both of which can implement dynamic bytecode operations. The biggest difference is that the former creates a DEX file, and then The alternative is to create a CLASS file.

Using DexMaker to dynamically create a class

This way of creating a compiled and runnable class at runtime is called "runtime bytecode manipulation". Using the DexMaker tool can create a DEX file, and then we decompile this DEX to see what the created class looks like.
public class MainActivity extends AppCompatActivity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate (savedInstanceState);
		setContentView(R.layout.activity_main);
	}

	public void onMakeDex(View view){
		try {
			DexMaker dexMaker = new DexMaker();
			// Generate a HelloWorld class.
			TypeId<?> helloWorld = TypeId.get("LHelloWorld;");
			dexMaker.declare(helloWorld, "HelloWorld.generated", Modifier.PUBLIC, TypeId.OBJECT);
			generateHelloMethod(dexMaker, helloWorld);
			// Create the dex file and load it.
			File outputDir = new File(Environment.getExternalStorageDirectory() + File.separator + "dexmaker");
			if (!outputDir.exists())outputDir.mkdir();
			ClassLoader loader = dexMaker.generateAndLoad(this.getClassLoader(), outputDir);
			Class<?> helloWorldClass = loader.loadClass("HelloWorld");
			// Execute our newly-generated code in-process.
			helloWorldClass.getMethod("hello").invoke(null);
		} catch (Exception e) {
			Log.e("MainActivity","[onMakeDex]",e);
		}
	}

	/**
	 * Generates Dalvik bytecode equivalent to the following method.
	 *    public static void hello() {
	 *        int a = 0xabcd;
	 *        int b = 0xaaaa;
	 *        int c = a - b;
	 *        String s = Integer.toHexString(c);
	 *        System.out.println(s);
	 *        return;
	 *    }
	 */
	private static void generateHelloMethod(DexMaker dexMaker, TypeId<?> declaringType) {
		// Lookup some types we'll need along the way.
		TypeId<System> systemType = TypeId.get(System.class);
		TypeId<PrintStream> printStreamType = TypeId.get(PrintStream.class);

		// Identify the 'hello()' method on declaringType.
		MethodId hello = declaringType.getMethod(TypeId.VOID, "hello");

		// Declare that method on the dexMaker. Use the returned Code instance
		// as a builder that we can append instructions to.
		Code code = dexMaker.declare(hello, Modifier.STATIC | Modifier.PUBLIC);

		// Declare all the locals we'll need up front. The API requires this.
		Local<Integer> a = code.newLocal(TypeId.INT);
		Local<Integer> b = code.newLocal(TypeId.INT);
		Local<Integer> c = code.newLocal(TypeId.INT);
		Local<String> s = code.newLocal(TypeId.STRING);
		Local<PrintStream> localSystemOut = code.newLocal(printStreamType);

		// int a = 0xabcd;
		code.loadConstant(a, 0xabcd);

		// int b = 0xaaaa;
		code.loadConstant(b, 0xaaaa);

		// int c = a - b;
		code.op(BinaryOp.SUBTRACT, c, a, b);

		// String s = Integer.toHexString(c);
		MethodId<Integer, String> toHexString
				= TypeId.get(Integer.class).getMethod(TypeId.STRING, "toHexString", TypeId.INT);
		code.invokeStatic(toHexString, s, c);

		// System.out.println(s);
		FieldId<System, PrintStream> systemOutField = systemType.getField(printStreamType, "out");
		code.sget(systemOutField, localSystemOut);
		MethodId<PrintStream, Void> printlnMethod = printStreamType.getMethod(
				TypeId.VOID, "println", TypeId.STRING);
		code.invokeVirtual(printlnMethod, null, localSystemOut, s);

		// return;
		code.returnVoid();
	}

}

After running, find the newly created file "Generated1532509318.jar" in the dexmaker directory of the SD card, decompress the "classes.dex" inside, and then use the "dex2jar" tool to convert it into a jar file, and finally use "jd-gui" "tool to decompile the source code of the jar.

So far, we have successfully created a compiled class (DEX) at runtime.

Modifying the target Activity that needs to be activated The

next problem is how to replace the PlugActivity that needs to be activated and is not registered in the Manifest with the registered TargetActivity.

In Android, when the virtual machine loads a class, it is through the loadClass method of ClassLoader, and the loadClass method is not a final type, which means that we can create our own class to inherit ClassLoader to overload the loadClass method and rewrite the class loading logic. , when you need to load PlugActivity, secretly replace it with TargetActivity.

The general idea is as follows:
public class CJClassLoader extends ClassLoader{

@override
    public Class loadClass(String className){
      if(current context plugin is not empty) {
        if( className 是 TargetActivity){
             Find the original PlugActivity to be loaded and dynamically create the dex file of the class (TargetActivity extends PlugActivity)
             return TargetActivity loaded from dex file
        }else{
             return Use the corresponding PluginClassLoader to load ordinary classes
        }  
     }else{
         return super.loadClass() //Use the original class loading method
     }   
    }
}

In this way, the PlugActivity in the startup plugin can be turned into a dynamically created TargetActivity.

But there is still a problem, when the main project starts the plug-in Activity, we can replace the Activity, but what if the plug-in Activity (such as MainActivity) starts another Activity (SubActivity)? The plug-in is a common third-party APK, and we cannot change the logic of jumping Activity inside. In fact, when starting the plug-in MainActivity from the main project, it actually starts the TargetActivity (extends MainActivity) that we dynamically created, and we know that when the Activity starts another Activity, it uses its "startActivityForResult" method, so we can create TargetActivity. , rewrite its "startActivityForResult" method so that it can also dynamically create an Activity when it starts other Activity, so that the problem can be solved.

Dynamically creating Activity open source project android-pluginmgr

This brain-opening dynamic loading idea comes from houkx's open source project android-pluginmgr.

There are three ClassLoaders in the android-pluginmgr project, one is the CJClassLoader used to replace the Application of the host APK, the other is the PluginClassLoader used to load the plug-in APK, and the second is the DexClassLoader used to load the dex package of the PlugActivity dynamically generated when the plug-in Activity is started ( Stored in the Map collection proxyActivityLoaderMap). Among them, CJClassLoader is the Parent of PluginClassLoader, and PluginClassLoader is the Parent of the third DexClassLoader.

When the ClassLoader class loads the Class, the Parent's ClassLoader will be used first, but when the Parent cannot complete the loading work, the Child's ClassLoader will be called to complete the work.

quote
java.lang.ClassLoader
Loads classes and resources from a repository. One or more class loaders are installed at runtime. These are consulted whenever the runtime system needs a specific class that is not yet available in-memory. Typically, class loaders are grouped into a tree where child class loaders delegate all requests to parent class loaders. Only if the parent class loader cannot satisfy the request, the child class loader itself tries to handle it.

For specific analysis, please refer to the working mechanism of Android dynamically loading the basic ClassLoader .

Therefore, each time an Activity is loaded, the loadClass method of the top-level CJClassLoader will be called, so as to ensure that the plug-in Activity can be successfully replaced with PlugActivity. Of course, how to control the loading of the three ClassLoaders is also one of the design difficulties of the pluginmgr project.

Therefore, each time an Activity is loaded, the loadClass method of the top-level CJClassLoader will be called, so as to ensure that the plug-in Activity can be successfully replaced with PlugActivity. Of course, how to control the loading of the three ClassLoaders is also one of the design difficulties of the pluginmgr project.

Existing Problems

The way of creating dynamic classes makes registering a general Activity more useful to Activity. The problems with this approach are also obvious:
  • Use the same registered Activity, so some properties that need to be registered in the Manifest cannot be customized for each Activity;
  • The permissions in the plug-in cannot be dynamically registered. The permissions required by the plug-in must be registered in the host, and the permissions cannot be dynamically added;
  • The Activity of the plugin cannot start an independent process, because it needs to be registered in the Manifest;
  • Dynamic bytecode operation involves Hack development, so it is unstable compared to the proxy mode;

Among them, the unstable problem appears in the support of Service. The Activity and Broadcast Receiver can be handled by dynamically creating classes, but it is not possible to handle the Service in a similar way, because "ContextImpl.getApplicationContext" expects to get a non-ContextWrapper context. If not, continue to the next cycle. The current Context instances are all wrappers, so it will enter an infinite loop.

According to houkx, he now has another idea to achieve the purpose of "starting as a normal third-party APK for installation", and it is not based on the principle of dynamic class creation, looking forward to the update of his open source project.

The difference between the proxy Activity mode and the dynamically created Activity mode

Simply put, the biggest difference is that the proxy mode uses a proxy Activity, while the dynamically created Activity mode uses a general Activity.

In the proxy mode, a proxy Activity is used to complete the work that should be done by the plug-in Activity. This proxy Activity is a standard Android Activity component with a life cycle and context environment (ContextWrapper and ContextCompl), but it is just an empty shell. It does not undertake any business logic; the plug-in Activity is actually just an ordinary Java object, which has no context, but can execute the code of business logic normally. Proxy Activity and different plug-in Activity cooperate to complete different business logic. So the proxy mode actually still uses the conventional Android development technology, but when processing plug-in resources, the system's hidden API is forced to be called (unless some ROMs have modified this API), so this mode can still work stably and upgrade. .

Dynamically create Activity mode, the dynamically created Activity class is registered in the main project, it is a standard Activity, it has its own Context and life cycle, and does not require proxy Activity.

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=326667787&siteId=291194637