Git code
Principle of thermal renewal
Hot update/hot fix
Do not install the new version of the software, directly download new function modules from the network to update the software
The difference between hot updates and plug-ins
There are two differences
- Plug-in content is not in the original App, while hot update is the content in the original App has been changed
- Plug-ins have fixed entry points in the code, while hot updates can change the code anywhere
The principle of hot renewal
The dex file of the ClassLoader is replaced by directly modifying the bytecode
Preknowledge: the classloading process of loadClass()
- Macroscopically: a cached, top – to – bottom loading process (also known online as “parent power machine”)
“)
- For a specific ClassLoader:
- I’m going to fetch it from my cache
- If you don’t have a cache, ask the parent ClassLoader (parent.loadClass()).
- The parent View doesn’t have one, so it loads it (findClass()).
- FindClass () for BaseDexClassLoader or a subclass of it (DexClassLoader, PathClassLoader, etc.):
- Through its pathlist.findClass ()
- It’s pathlist.loadClass () through DexPathList’s dexElements findClass()
- So the key to hot updates is to load the patch dex file into an Element and insert it in front of the dexElements array (which will be ignored if inserted later).
Handwriting is finer
- Because you can’t specify who to update before you update it; Instead of defining a new ClassLoader, you can only modify the ClassLoader so that it can load the classes in the patch
- Because the class of the patch already exists in the original App, the Element object of the patch should be inserted in front of the dexElements; it will be ignored after insertion.
- Specific approach: reflection
- Create a PathClassLoader yourself with a patch
- Replace elements in patch PathClassLoader with old ones
- Note:
- Loading hot update as soon as possible (general method is to put the loading process on the Application. The attachBaseContext ())
- After the hot update download is complete, kill the application if necessary for the patch to take effect
- Optimization: Hot update doesn’t have to type everything, just bring in the changed class and pack the specified class into the dex with D8
- Complete: Load from the Internet
- Reoptimize: Write the packaging process as a task
Whole package replacement (full replacement)
- First let’s write a page that displays the words “I want hot updates” provided by the Plugin class.
Public class Plugin {public static String getTitle(){return "I want to update "; }}Copy the code
- And then we modify the class to say “updated” when we zoom in.
Return "updated ";Copy the code
- Then we need to package the modified project as hotfix.apk and copy the whole package of apK to the assets/apk directory for easy replication
- And then we’re gonna go ahead and change it to say, “I want to hot update.”
- The code of hot update is prepared below, and the hot update is completed by clicking the hot update button, which mainly includes the following two steps
- 1. Copy the APK file
- 2. Replace the ClassLoader and load the new dex file
- Here is the code that calls it when hot update is clicked, and then kills the app to restart it
private void loadHotFix() { //1. File apk = new File(getCacheDir() + "hotfix.apk"); if (! apk.exists()) { try (Source source = Okio.source(getAssets().open("apk/hotfix.apk")); BufferedSink sink = Okio.buffer(Okio.sink(apk));) { sink.writeAll(source); } catch (IOException e) { e.printStackTrace(); OriginalLoader = getClassLoader(); originalLoader = getClassLoader(); DexClassLoader classLoader = new DexClassLoader(apk.getPath(),getCacheDir().getPath(),null,null); Class loaderClass = BaseDexClassLoader.class; Field pathListField = loaderClass.getDeclaredField("pathList"); pathListField.setAccessible(true); Object pathListObject = pathListField.get(classLoader); Class pathListClass = pathListObject.getClass(); Field dexElementsField = pathListClass.getDeclaredField("dexElements"); dexElementsField.setAccessible(true); Object dexElementsObject = dexElementsField.get(pathListObject); Object originalPathListObject = pathListField.get(originalLoader); dexElementsField.set(originalPathListObject,dexElementsObject); //originalLoader.pathList.dexElements = classLoader.pathList.dexElement } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); }}Copy the code
Below are the renderings
Mount the hot update code into the Application
The above hot update function needs to be triggered by clicking the button every time, which is obviously not what we need. What we need is to load the hot update as soon as possible when the App starts. Now we need to transplant the code into the Application
public class HotfixApp extends Application { @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); // Load hot update plugin loadHotFix(); }}Copy the code
Incremental replacement
In our development, hot updates tend to just update a few classes or resource files, and the above full replacement approach is unwieldy, so incremental replacement is a good choice
In the above code, we only need to replace the Plugin class. We can just compile and package the Plugin
- First modify plugin.java
Public class Plugin {public static String getTitle(){return "I'm dex hotfix "; }}Copy the code
- Then compile our plugin.java into a class file
javac Plugin.java Copy the code
- The class file is then compiled into a dex file
d8 Plugin.class Copy the code
- Finally, place the generated classes.dex file in assets/apk of your app
- Then modify the hot fix mount code, it is worth noting to insert the latest patch dex file at the front
public class HotfixApp extends Application { @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); // Load hot update plugin // full replacement //loadHotFix(); // Incrementally replace loadDexFix(); } /** * Incremental replacement to replace dex */ private void loadDexFix() {//1. File apk = new File(getCacheDir() + "hotfix.dex"); if (! apk.exists()) { try (Source source = Okio.source(getAssets().open("apk/hotfix.dex")); BufferedSink sink = Okio.buffer(Okio.sink(apk));) { sink.writeAll(source); } catch (IOException e) { e.printStackTrace(); OriginalLoader = getClassLoader(); originalLoader = getClassLoader(); DexClassLoader classLoader = new DexClassLoader(apk.getPath(),getCacheDir().getPath(),null,null); Class loaderClass = BaseDexClassLoader.class; Field pathListField = loaderClass.getDeclaredField("pathList"); pathListField.setAccessible(true); Object pathListObject = pathListField.get(classLoader); Class pathListClass = pathListObject.getClass(); Field dexElementsField = pathListClass.getDeclaredField("dexElements"); dexElementsField.setAccessible(true); Object dexElementsObject = dexElementsField.get(pathListObject); Object originalPathListObject = pathListField.get(originalLoader); Object originalDexElementsObject = dexElementsField.get(originalPathListObject); / / Array operations, the latest patches dex file is inserted into the front int oldLength = Array. The getLength (originalDexElementsObject); int newLength = Array.getLength(dexElementsObject); Object concatDexElementsObject = Array.newInstance(dexElementsObject.getClass().getComponentType(), oldLength + newLength); for (int i = 0; i < newLength; i++) { Array.set(concatDexElementsObject, i, Array.get(dexElementsObject, i)); } for (int i = 0; i < oldLength; i++) { Array.set(concatDexElementsObject, newLength + i, Array.get(originalDexElementsObject, i)); } dexElementsField.set(originalPathListObject, concatDexElementsObject); / / the whole amount to replace pseudo code - > originalLoader. PathList. DexElements = this. PathList. DexElement / / increment replacing pseudo code - > originalLoader.pathList.dexElements += classLoader.pathList.dexElement } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); }}}Copy the code
Below are the renderings
The process to perfect
Add a “remove button” and a “kill app” button for hot updates
onClick{ ... // Load the text case r.d.howText: tv_hotfix.settext (plugin.getTitle ()); break; // Load the hot update dex file case r.i.HotFix: loadHotFix(); break; // Remove hot update case r.i.RemoveHotfix: File apk = new File(getCacheDir() + "/hotfix.dex"); if (apk.exists()){ apk.delete(); } break; Strong / / kill Process case R.i d.k illSelf: android. OS. Process. KillProcess (android. OS. Process. MyPid ()); break; . }Copy the code
Now operate the buttons
- [display text] -> “I want hot update”
- [Hot Update] (Load dex fix file)
- [Kill the app] (Restart the app)
- -> “I am the title of dex hotfix”
- [Remove Hot Update] (Remove hot update dex)
- [Kill the app] (Restart the app)
- [display text] -> “I want hot update”
- .
This completes a complete hot update demo
Download patches from the network
Hotupdate patches cannot be stored locally during actual development and are usually stored on the server
Case r.i.HotFix: OkHttpClient client = new OkHttpClient(); final Request request = new Request.Builder() .url("https://api.dsh.com/patch/upload/hotfix.dex") .build(); client.newCall(request) .enqueue(new Callback() { @Override public void onFailure(@NotNull Call call, @NotNull IOException e) { v.post(new Runnable() { @Override public void run() { Toast.makeText(MainActivity.this, "Wrong ", toast.length_short).show(); }}); } @Override public void onResponse(@NotNull Call call, @NotNull Response response) { try (BufferedSink sink = Okio.buffer(Okio.sink(apk))) { sink.write(response.body().bytes()); } catch (IOException e) { e.printStackTrace(); } v.post(new Runnable() {@override public void run() {toast.maketext (mainactivity.this, "patch loading successfully ", Toast.LENGTH_SHORT).show(); }}); }}); break;Copy the code
Automatic patch packaging
Add the following code to build.gradle and execute the package command. This will output the dex file of the patch
def patchPath = 'com.dsh.txlessons.plugin.utils/Plugin' task hotfix { doLast { exec { commandLine 'rm', '-r', './build/patch' } exec { commandLine 'mkdir', './build/patch' } exec { commandLine 'javac', "./src/main/java/${patchPath}.java", '-d', '. / build/patch '} exec {commandLine '/ Users/DSH/Library/Android/SDK/build - the tools / 29.0.2 / d8', "./build/patch/${patchPath}.class", '--output', './build/patch' } exec { commandLine 'mv', "./build/patch/classes.dex", './build/patch/hotfix.dex' } } }Copy the code