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

  1. Plug-in content is not in the original App, while hot update is the content in the original App has been changed
  2. 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
  1. Create a PathClassLoader yourself with a patch
  2. Replace elements in patch PathClassLoader with old ones
  3. Note:
    1. Loading hot update as soon as possible (general method is to put the loading process on the Application. The attachBaseContext ())
    2. After the hot update download is complete, kill the application if necessary for the patch to take effect
  4. 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
  5. Complete: Load from the Internet
  6. 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