In the current software and hardware environment, Native apps and Web apps have obvious advantages in user experience, but in actual projects, some clients are frequently upgraded due to frequent business changes, this results in poor user experience, which is precisely the advantage of Web apps. This article will sort out and share with you the information about how Android dynamically loads jar files on the Internet, and try to improve the frequent upgrade.
In general, Android Application Development can meet our general needs through conventional development methods and code architecture. However, some special problems often lead to further meditation. We generate an epiphany from our meditation, which leads to a new form of technology.
How can I develop an Android Application with custom controls? Just like eclipse, plug-ins can be dynamically loaded; how can Android applications execute unpredictable code on the server? How to encrypt the Android app and perform self-decryption only during execution to prevent the app from being cracked ?......
A friend familiar with Java technology may realize that we need to use the class loader to flexibly load and execute classes. This is already a mature technology in Java, but in Android, most of us are still very unfamiliar.
Class Loading Mechanism
Like other Java virtual machines, the Dalvik virtual machine first needs to load the corresponding class into the memory when running the program. In Java standard virtual machines, class loading can be read from class files or binary streams in other forms. Therefore, we often use this, manually load the Class when the program runs, so that the code can be dynamically loaded and executed.
However, Dalvik virtual machines are not standard Java virtual machines after all. Therefore, there are similarities and differences in class loading mechanisms. We must treat them differently
For example, when using a standard Java virtual machine, we often customize class loaders that inherit from ClassLoader. Then, use the defineClass method to load the Class from a binary stream. However, this is not feasible in Android, so there is no need to make a detour. Refer to the source code and we know that the defineClass method of ClassLoader in Android is to call the defineClass local static method of VMClassLoader. In addition to throwing an "UnsupportedOperationException", this local method does nothing, and even returns NULL values.
static void Dalvik_java_lang_VMClassLoader_defineClass(const u4* args,JValue* pResult){ Object* loader = (Object*) args[0]; StringObject* nameObj = (StringObject*) args[1]; const u1* data = (const u1*) args[2]; int offset = args[3]; int len = args[4]; Object* pd = (Object*) args[5]; char* name = NULL; name = dvmCreateCstrFromString(nameObj); LOGE("ERROR: defineClass(%p, %s, %p, %d, %d, %p)\n",loader, name, data, offset, len, pd); dvmThrowException("Ljava/lang/UnsupportedOperationException;","can't load this type of class file"); free(name); RETURN_VOID();}
Dalvik Virtual Machine class loading mechanism
If ClassLoader is difficult in the Dalvik virtual machine, how can we achieve dynamic loading of classes? Android derives two classes from ClassLoader: DexClassLoader and PathClassLoader. Note the code commented out in the PathClassLoader:
/* --this doesn't work in current version of Dalvik-- if (data != null) { System.out.println("--- Found class " + name + " in zip[" + i + "] '" + mZips[i].getName() + "'"); int dotIndex = name.lastIndexOf('.'); if (dotIndex != -1) { String packageName = name.substring(0, dotIndex); synchronized (this) { Package packageObj = getPackage(packageName); if (packageObj == null) { definePackage(packageName, null, null, null, null, null, null, null); } } } return defineClass(name, data, 0, data.length); }*/
This also proves that the defineClass function is indeed castrated in the Dalvik virtual machine. The classloaders inherited from ClassLoader essentially reload the findClass method of ClassLoader. When executing loadClass, we can refer to the ClassLoader source code:
protected Class
loadClass(String className, boolean resolve) throws ClassNotFoundException {Class
clazz = findLoadedClass(className); if (clazz == null) { try { clazz = parent.loadClass(className, false); } catch (ClassNotFoundException e) { // Don't want to see this. } if (clazz == null) { clazz = findClass(className); } } return clazz;}
Therefore, DexClassLoader and PathClassLoader are both class loaders that comply with the parent-child delegate model (because they do not load the loadClass method ). That is to say, before loading a class, they should go back and check whether they and their above class loaders have loaded the class. If it has already been loaded, it will directly return it, instead of loading it again.
DexClassLoader and PathClassLoader both use the DexFile class to load classes. By the way, the Dalvik virtual machine recognizes dex files rather than class files. For this reason, the files we provide for the category can only be dexfiles, including. APK or. jar files with dexfiles.
Some people may think that, since DexFile can directly load classes, why should we use ClassLoader subclasses? When loading a class, DexFile calls the member method loadClass or loadClassBinaryName. Specifically, loadClassBinaryName needs to convert "." In the class name that contains the package name to "/". Let's take a look at the loadClass code:
public Class loadClass(String name, ClassLoader loader) { String slashName = name.replace('.', '/'); return loadClassBinaryName(slashName, loader);}
There is a comment before this Code. The key part of the screenshot is: If you are not calling this from a class loader, this is most likely not going to do what you want. use {@ link Class # forName (String)} instead. this is why we need to use the ClassLoader subclass. As to how it verifies whether this method is called in ClassLoader, I have not studied it. If you are interested, you can continue to explore it.
There is a detail that may not be easily noticed. PathClassLoader uses the constructor new DexFile (path) to generate the DexFile object, while DexClassLoader obtains the DexFile object through its static method loadDex (path, outpath, 0.
The difference between the two lies in dexclassloader‑release an outpath‑writable file to release the dex file in the. APK package or. jar package. In other words, PathClassLoader cannot release dex from the zip package. Therefore, it only supports direct operations on dex files, or the installed apk (because the installed apk has cached dex files in the cache ). Dexclassloadercan support .apk,. jar, and. dex files, and release dex files in the specified outpath.
In addition, PathClassLoader calls the loadClassBinaryName of DexFile when loading classes, while DexClassLoader calls loadClass. Therefore, when PathClassLoader is used, the class full name must be replaced "."
Actual Operation
The tools used are common: The dx tool, such as javac, dx, and eclipse, is best to specify -- no-strict, because the path of the class file may not match
After a class is loaded, we can usually use this class through the Java reflection mechanism, but this is relatively inefficient, and the old reflection code is also complicated and messy. A better way is to define an interface and write it into the container. The Class to be loaded inherits from this interface and has a constructor with null parameters. This allows us to generate an object through the newInstance method of the Class and forcibly convert the object to an interface object, so you can directly call the member method. The following is the specific implementation steps:
Step 1:
Write dynamic code:
Package com. dynamic. interfaces; import android. app. activity;/*** dynamic loading class interface */public interface IDynamic {/** Initialization Method */public void init (Activity activity ); /** custom Method */public void showBanner (); public void showDialog (); public void showFullScreen (); public void showAppWall (); /** destruction method */public void destory ();}
The implementation class code is as follows:
Package com. dynamic. impl; import android. app. activity; import android. widget. toast; import com. dynamic. interfaces. IDynamic;/*** Dynamic class implementation **/public class Dynamic implements IDynamic {private Activity mActivity; @ Overridepublic void init (Activity activity) {mActivity = activity ;} @ Overridepublic void showBanner () {Toast. makeText (mActivity, "I'm the ShowBannber method", 1500 ). show () ;}@ Overridepublic void showDialog () {Toast. makeText (mActivity, "I'm the ShowDialog method", 1500 ). show () ;}@ Overridepublic void showFullScreen () {Toast. makeText (mActivity, "I'm the ShowFullScreen method", 1500 ). show () ;}@ Overridepublic void showAppWall () {Toast. makeText (mActivity, "I'm the ShowAppWall method", 1500 ). show () ;}@ Overridepublic void destory (){}}
In this way, dynamic classes are developed.
Step 2:
Package the Dynamic classes developed above into. jar. Note that only the Dynamic. java class is packaged, and the interface class IDynamic. java is not packaged,
Then pack the packageCopy the jar file to the platform-tools directory in the android installation directory., Use the dx command: (my jar file is dynamic. jar)
Dx -- dex -- output = dynamic_temp.jar dynamic. jar
In this way, dynamic_temp.jar is generated. What is the difference between this jar and dynamic. jar?
In fact, the main work of this command is: the ghost file can be used, which will be discussed later.
It's not over yet, because you think about how to connect dynamic classes and Target classes? That is the dynamic class interface, so we need to create a. jar package. At this time, we only need to call the interface class IDynamic. java.
Then reference the. jar file to the target class. The following describes the implementation of the target class:
Package com. jiangwei. demo; import java. io. file; import java. util. list; import android. app. activity; import android. content. intent; import android. content. pm. activityInfo; import android. content. pm. packageManager; import android. content. pm. resolveInfo; import android. OS. bundle; import android. OS. environment; import android. view. view; import android. widget. button; import android. widget. toast; import com. dynamic. interfaces. IDynamic; import dalvik. system. dexClassLoader; import dalvik. system. pathClassLoader; public class AndroidDynamicLoadClassActivity extends Activity {// dynamic class loading interface private IDynamic lib; @ Override public void onCreate (Bundle savedInstanceState) {super. onCreate (savedInstanceState); setContentView (R. layout. main); // initialize the component Button showBannerBtn = (Button) findViewById (R. id. show_banner_btn); Button showDialogBtn = (Button) findViewById (R. id. show_dialog_btn); Button showFullScreenBtn = (Button) findViewById (R. id. show_fullscreen_btn); Button showAppWallBtn = (Button) findViewById (R. id. show_appwall_btn);/** use DexClassLoader to load the class * // dex compressed file path (which can be apk, jar, or zip) String dexPath = Environment. getExternalStorageDirectory (). toString () + File. separator + "Dynamic.apk"; // dex unzip the released directory // String dexOutputDir = getApplicationInfo (). dataDir; String dexOutputDirs = Environment. getExternalStorageDirectory (). toString (); // define DexClassLoader // The first parameter: the path of the dex compressed file // The second parameter: The Directory stored after dex decompression // The third parameter: is the local library file directory dependent on C/C ++, which can be null // The fourth parameter: The Class Loader DexClassLoader cl = new DexClassLoader (dexPath, dexOutputDirs, null, getClassLoader ();/** use the PathClassLoader method to load the class * // create an intent to find the specified apk: com. dynamic. impl indicates AndroidMainfest in the specified apk. intent intent = new Intent ("com. dynamic. impl ", null); // get PackageManager pm = getPackageManager (); List
Resolveinfoes = pm. queryIntentActivities (intent, 0); // obtain the information of the specified activity ActivityInfo actInfo = resolveinfoes. get (0 ). activityInfo; // obtain the apk directory or jar directory String apkPath = actInfo. applicationInfo. sourceDir; // native code directory String libPath = actInfo. applicationInfo. nativeLibraryDir; // create a class loader and load dex to the virtual machine. // The first parameter is the path for installing the apk. Note that the path can only be actInfo. applicationInfo. sourceDir to get // The second parameter: it is the local library file directory dependent on C/C ++, which can be null // third Parameters: PathClassLoader pcl = new PathClassLoader (apkPath, libPath, this. getClassLoader (); // load class try {// com. dynamic. impl. dynamic is a Dynamic Class name // use DexClassLoader to load the Class // Class libProviderClazz = cl. loadClass ("com. dynamic. impl. dynamic "); // use PathClassLoader to load Class libProviderClazz = pcl. loadClass ("com. dynamic. impl. dynamic "); lib = (IDynamic) libProviderClazz. newInstance (); if (lib! = Null) {lib. init (AndroidDynamicLoadClassActivity. this) ;}} catch (Exception exception) {exception. printStackTrace ();}/** call the methods in the dynamic class */showBannerBtn respectively. setOnClickListener (new View. onClickListener () {public void onClick (View view) {if (lib! = Null) {lib. showBanner ();} else {Toast. makeText (getApplicationContext (), "failed to load class", 1500 ). show () ;}}); showDialogBtn. setOnClickListener (new View. onClickListener () {public void onClick (View view) {if (lib! = Null) {lib. showDialog ();} else {Toast. makeText (getApplicationContext (), "failed to load class", 1500 ). show () ;}}); showFullScreenBtn. setOnClickListener (new View. onClickListener () {public void onClick (View view) {if (lib! = Null) {lib. showFullScreen ();} else {Toast. makeText (getApplicationContext (), "failed to load class", 1500 ). show () ;}}); showAppWallBtn. setOnClickListener (new View. onClickListener () {public void onClick (View view) {if (lib! = Null) {lib. showAppWall ();} else {Toast. makeText (getApplicationContext (), "failed to load class", 1500 ). show ();}}});}}
Here, an IDynamic interface variable is defined, and DexClassLoader and PathClassLoader are used to load classes. The following describes how to load DexClassLoader:
// Define DexClassLoader // The first parameter: Path of the dex compressed file // The second parameter: Directory stored after dex decompression // The third parameter: is the local library file directory dependent on C/C ++, which can be null // The fourth parameter: The Class Loader DexClassLoader cl = new DexClassLoader (dexPath, dexOutputDirs, null, getClassLoader ());
As mentioned above, DexClassLoader inherits the ClassLoader class. The parameters are described below:
The first parameter is the path of the dex compressed file: This is the directory of dynamic_temp.jarwhich we will compile. It can also be in the .zipand .apk format.
The second parameter is the directory that dex stores after decompression: the directory that stores the dex file decompressed from the .jar,.zip,.apk file, which is different from the PathClassLoader method, at the same time, you can also see that the PathClassLoader method does not have this parameter, which is really the difference between the two classes:
PathClassLoader cannot release dex from the zip package. Therefore, it only supports direct operations on dex files, or the installed apk (because the installed apk has cached dex files in the data/dalvik directory of the mobile phone ). Dexclassloadercan support .apk,. jar, and. dex files, and release dex files in the specified outpath.
However, we can use the DexClassLoader method to specify the directory where the extracted dex files are stored. However, we generally do not do this, because this will undoubtedly expose the dex files, so we generally will not. jar /. zip /. the apk compressed file is stored in a location that can be noticed by the user, while extracting the dex directory is not visible to the user.
The third and fourth parameters do not use much, so we will not explain them too much here.
Note that when the PathClassLoader method is used, the first parameter is the path stored by dex. The following is passed:
// Obtain the apk directory or jar directory String apkPath = actInfo. applicationInfo. sourceDir;
The specified apk installation path. This value can only be obtained in this way, otherwise the class will fail to be loaded.
Step 3:
Running target class:
The work to be done is:
If the DexClassLoader method is used to load the class: At this time, the .jar).zip0000.apk file will be put in the specified directory. I will put it in the root directory of the SD card for convenience.
If you use the PathClassLoader method to load the class: At this time, wait for the worker to first install dynamic.apk to the mobile phone. Otherwise, the activity cannot be found. Note that:
// Create an intent to find the specified apk: com. dynamic. impl indicates AndroidMainfest in the specified apk. intent intent = new Intent ("com. dynamic. impl ", null );
Com. dynamic. impl is an action that needs to be defined in the specified apk. This name is agreed between the dynamic apk and the target apk.
Running result
Click showBanner to display a Toast. The code in the dynamic class is successfully run!
A better solution is to obtain the dynamic .jar.zip.apk file from the network, which is secure and reliable. At the same time, the local target project can execute different logic without modifying the code.
Some Ideas about code encryption
Initially, the dex file is encrypted and then the decryption code is written in the Native layer through JNI. After decryption, the binary stream is directly uploaded, and then the class is loaded into the memory through defineClass.
This can also be done now, but because defineClass cannot be used directly, the file path must be passed to the dalvik Virtual Machine kernel. Therefore, the decrypted files must be written to the disk, increasing the risk of being cracked.
Dalvik Virtual Machine kernel only supports loading classes from dex files, which is not flexible, i'm not sure whether the Dalvik virtual machine itself does not support it or Android castrated it during transplantation. However, we believe that Dalvik or the Android open-source project is working towards supporting raw data definition classes.
We can see in the document that Google says: Jar or APK file with "classes. dex ". (May expand this to include "raw DEX" in the future .); we can also see RawDexFile in the Dalvik source code of Android (but there is no specific implementation)
Before RawDexFile is released, we can only use this encryption method that has certain risks. Pay attention to the released dex file path and permission management. In addition, after the class is loaded, the temporary decryption file should be deleted immediately unless for other purposes.