preface
The previous article analyzed the principles of Instant Run, and a brief summary of what happened previously:
The purpose of Instant Run is to make changes in the development process can be applied without completely compiling and reinstalling the app, which means that the actual effect of changes can be seen faster and save time. The implementation principle is to implement pegs in the initial compilation by modifying the original build process, compile only the changed parts in the later changes, push the product to the device, and load the new changes through the runtime embedded in the APP.
The Principles section only covers the principles, not the actual code of the Instant Run framework. Since all of Android’s code is open-source through AOSP, without further ado,
Read The Fucxxxx Source Code — Linus
To prepare
Access to the source code
AOSP source code is an extremely large project. Because of its size, Google manages it using repo, a version management tool developed based on Git. If all the open source code down, dozens of G. In fact, to see the code related to Instant Run, you only need a few related Git repositories, and you don’t need to use repo to read all the source code.
clone
As mentioned earlier, The design of Instant Run requires the collaboration of Android build tools and Android Studio, so the relevant source code is in both libraries.
- Android.googlesource.com/platform/to…This library has Android Gradle plugin code, instant-run framework code all in which
instant-run
In the directory- It’s important to note that because of the latest Android Studio, Google is using the new one
apply change
The architecture replaces instant-run, so it doesn’t appear in the latest code. Cut toStudio – 3.2.1 this tagYou can see it
- It’s important to note that because of the latest Android Studio, Google is using the new one
- Android.googlesource.com/platform/to…This library contains the source code for Android Studio, where you can see how AS works with Instant-Run
- Again, switch to the same tag as above
An error to solve
The Timed out error may occur while git Clone is in use.
Git config --global http.proxy http://127.0.0.1:1080 # 127.0.0.1:1086 git config --global HTTPS. Proxy https://127.0.0.1:1080 # Such as socks5: / / 127.0.0.1:1086Copy the code
Remember to remove the proxy after downloading, so as not to affect the subsequent use of Git
git config --global --unset http.proxy
git config --global --unset https.proxy
Copy the code
Import the project
tools/base
Clone tools/ Base to Studio -3.2.1
instant-run
Choose Android Studio to read the source code of the Instant-Run framework.
In the instant-run directory, the following information is displayed:
.├ ─ BUILD ├─ Annotations.imL │ ├─ BUILD │ ├─ Android.sdkTools ├─ build. ├ le ├─ SRC ├─ Run-client ├─ Android. Sdktools. Run-client The SRC ├ ─ ─ instant - run - common │ ├ ─ ─ android. Sdktools. Instant - run - common. On iml │ ├ ─ ─ build. Gradle │ └ ─ ─ the SRC ├ ─ ─ Instant - run - the runtime │ ├ ─ ─ BUILD │ ├ ─ ─ BUILD. Gradle │ ├ ─ ─ instant - run - runtime. On iml │ └ ─ ─ the SRC ├ ─ ─ instant - run - server │ ├ ─ ─ Androidmanifest.xml │ ├─ BUILD │ ├─ build.gradle │ ├─ design.properties │ ├─ SRC ├─ instant-run.imlCopy the code
As can be seen, the design of the instant-Run framework is clearly divided into Annotations, Client, Common, Runtime, and Server modules. This is actually a C/S architecture, which will be discussed later. Each of these modules has build.gradle, which can be seen as a gradle-managed project. For ease of reading, we want these modules to be in the same Gradle project when we open AS.
- First open the directory with AS. Then run it in this directory using any version of Gradle
wrapper
Task. (This requires running Gradle on your computer, which is a must for Android development.) - The familiar gradle directory, gradlew, gradlew. Bat files have been generated, just as you would in a normal Android project
- Create one manually
setting.gradle
And addinclude ':instant-run-annotations', ':instant-run-client', ':instant-run-common', ':instant-run-runtime', ':instant-run-server'
- According to the similar
compile project(':base:instant-run:instant-run-runtime')
tocompile project(':instant-run-runtime')
Changes to build. Gradle dependencies.provide files(androidJar)
tocompile files(androidJar)
, remove all kinds oftestCompile
Get rid ofapply plugin: 'jacoco-tools-base'
- Manually copy the source code of NonNull, Nullable annotations in the appropriate place, and sync once again, the project is imported, most of the error is gone, the code reference between different modules can also jump directly, you can enjoy the code
build-system/instant-run-instrumentation
Gradle can also be read using AS by modifying build.gradle.
Build-system is the entire Android Gradle plugin code.
adt/idea
Adt/IDEA source projects have a lot of code and don’t need to peruse them. Sublime Text is used to open them and search for them.
Read the source code
Principles of Reference Cross-reading works better. Analyze only hotswap-related code.
Inject building process
As analyzed in the principles, Instant Run needs to be connected to the construction process, and the server and Runtime parts of the framework are added to the APP. Let’s see how AS tells the Gradle plugin to do this.
Add to build. Gradle in your project
println getGradle(a).getStartParameter(a)
Copy the code
For a normal run application, the build TAB at the bottom of the AS should see output:
StartParameter{taskRequests=[DefaultTaskExecutionRequest{args=[:app:assembleDebug],projectPath='null'}], excludedTaskNames=[], currentDir=/Users/wuyi/Android/code/demo/Instantruntest, searchUpwards=true, projectProperties={android.optional.compilation=INSTANT_DEV,FULL_APK, android.injected.build.density=xxhdpi, android.injected.coldswap.mode=MULTIAPK, android.injected.build.api=22, android.injected.invoked.from.ide=true, android.injected.build.abi=arm64-v8a,armeabi-v7a,armeabi, android.injected.restrict.variant.name=debug, android.injected.restrict.variant.project=:app}, systemPropertiesArgs={}, gradleUserHomeDir=/Users/wuyi/.gradle, GradleHome = / Users/wuyi /. Gradle/wrapper/dists/gradle - 4.6 - all/bcst21l2brirad8k2ben1letg/gradle - 4.6, logLevel = LIFECYCLE, showStacktrace=INTERNAL_EXCEPTIONS, buildFile=null, initScripts=[], dryRun=false, rerunTasks=false, recompileScripts=false, offline=false, refreshDependencies=false, parallelProjectExecution=true, configureOnDemand=false, maxWorkerCount=4, buildCacheEnabled=false, interactive=false}Copy the code
This is the command used by AS to start Gradle with all the parameters. Note that projectProperties = {pilation = android.optional.com INSTANT_DEV, FULL_APK, this sentence, there is in favour of open instant – run. The android Gradle plugin’s execution logic is translated into the following enumeration definitions, which represent different compilation types:
package com.android.builder.model;
/** * enum describing possible optional compilation steps. This can be used to turn on java byte code * manipulation in order to support instant reloading, or profiling, or anything related to * transforming java compiler .class files before they are processed into .dex files. */
public enum OptionalCompilationStep {
/** * presence will turn on the InstantRun feature. */
INSTANT_DEV,
/** * Force rebuild of cold swap artifacts. * * Dex files and/or resources.ap_ for ColdswapMode.MULTIDEX and some split APKs for * ColdswapMode.MULTIAPK. */
RESTART_ONLY,
/** * Force rebuild of fresh install artifacts. * * A full apk for ColdswapMode.MULTIDEX and all the split apks for ColdswapMode.MULTIAPK. */
FULL_APK,
}
Copy the code
Without these parameters, it does not interfere with the normal build process.
Then, we learned that part of the instant-Run framework was typed into our APK package in order to support subsequent loading of hot updates. AS did this under the table. When the Android Gradle plug-in is released, it contains the jar package of instant- Run. When building, it is decompressed from it and placed in a specific location and then typed into our app. In the app/build/intermediates/incremental – the runtime – classess/find instant – run – jar.
! [image-20190918173101389](/Users/wuyi/Library/Application Support/typora-user-images/image-20190918173101389.png)
Com. Android. Build. Gradle. Tasks. Ir. FastDeployRuntimeExtractorTask class is responsible for the instant from gradle plug-in jar package – run – server. Jar extracted in the build directory:
/ we could just extract the instant-runtime jar and place it as a stream once we
// don't have to deal with AppInfo replacement.
@TaskAction
public void extract(a) throws IOException {
URL fdrJar =
FastDeployRuntimeExtractorTask.class.getResource(
"/instant-run/instant-run-server.jar");
if (fdrJar == null) {
throw new RuntimeException("Couldn't find Instant-Run runtime library"); }...Copy the code
Find our local cache gradle Android plugin jar package (usually in/Users/wuyi /. Gradle/caches/files/modules – 2-2.1 / com. Android. View the build/gradle/X.Y.Z/md 5/gradle-x.y.z.jar) to verify that there is an instant-run-server.jar inside
The location of the end of the build, the generated apk package is not the same as the location of the normal build process, this difference can be com. Android. Build. Gradle. Internal. Scope. See VariantScopeImpl, This specifies the location where the APK package is automatically installed by the AS after the build ends:
/**
* Obtains the location where APKs should be placed.
*
* @return the location for APKs
*/
@NonNull
@Override
public File getApkLocation(a) { String override = globalScope.getProjectOptions().get(StringOption.IDE_APK_LOCATION); File defaultLocation = getInstantRunBuildContext().isInInstantRunMode() ? getDefaultInstantRunApkLocation() : getDefaultApkLocation(); File baseDirectory = override ! =null && !variantData.getType().isHybrid()
? globalScope.getProject().file(override)
: defaultLocation;
return new File(baseDirectory, getVariantConfiguration().getDirName());
}
Copy the code
The artifacts and types of each build are recorded so that the AS and Gradle plug-in can determine which action to perform. The actual storage is the build.info.xml file, which can be found in the intermediates/build-info directory.
In AS side, com. Android. View idea. Fd. Gradle. InstantRunGradleUtils# getBuildInfo method is responsible for reading this message, InstantRunBuildInfo’s own parsing method is actually called in the instant-Run package.
@Nullable
public static InstantRunBuildInfo getBuildInfo(@NonNull AndroidModuleModel model) {
File buildInfo = getLocalBuildInfoFile(model);
if(! buildInfo.exists()) {return null;
}
String xml;
try {
xml = Files.toString(buildInfo, Charsets.UTF_8);
}
catch (IOException e) {
return null;
}
return InstantRunBuildInfo.get(xml);
}
Copy the code
Loading the patch needs to have a AppPathLoaderImpl class implements AbstractPatchesLoaderImpl getPatchedClasses method, specify the actual class have been restored, generation logic is as follows:
// com.android.build.gradle.internal.transforms.InstantRunTransform
/** * Use asm to generate a concrete subclass of the AppPathLoaderImpl class. * It only implements one method : * String[] getPatchedClasses(); * * The method is supposed to return the list of classes that were patched in this iteration. * This will be used by the InstantRun runtime to load all patched classes and register them * as overrides on the original classes.2 class files. * *@param patchFileContents list of patched class names.
* @param outputDir output directory where to generate the .class file in.
*/
private static void writePatchFileContents(
@NonNull ImmutableList<String> patchFileContents, @NonNull File outputDir, long buildId) {
ClassWriter cw = new ClassWriter(0);
MethodVisitor mv;
cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER,
IncrementalVisitor.APP_PATCHES_LOADER_IMPL, null,
IncrementalVisitor.ABSTRACT_PATCHES_LOADER_IMPL, null);
// Add the build ID to force the patch file to be repackaged.
cw.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC + Opcodes.ACC_FINAL,
"BUILD_ID"."J".null, buildId);
{
mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>"."()V".null.null);
mv.visitCode();
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL,
IncrementalVisitor.ABSTRACT_PATCHES_LOADER_IMPL,
"<init>"."()V".false);
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(1.1);
mv.visitEnd();
}
{
mv = cw.visitMethod(Opcodes.ACC_PUBLIC,
"getPatchedClasses"."()[Ljava/lang/String;".null.null);
mv.visitCode();
mv.visitIntInsn(Opcodes.BIPUSH, patchFileContents.size());
mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/String");
for (int index=0; index < patchFileContents.size(); index++) {
mv.visitInsn(Opcodes.DUP);
mv.visitIntInsn(Opcodes.BIPUSH, index);
mv.visitLdcInsn(patchFileContents.get(index));
mv.visitInsn(Opcodes.AASTORE);
}
mv.visitInsn(Opcodes.ARETURN);
mv.visitMaxs(4.1);
mv.visitEnd();
}
cw.visitEnd();
byte[] classBytes = cw.toByteArray();
File outputFile = new File(outputDir, IncrementalVisitor.APP_PATCHES_LOADER_IMPL + ".class");
try {
Files.createParentDirs(outputFile);
Files.write(classBytes, outputFile);
} catch (IOException e) {
throw newRuntimeException(e); }}Copy the code
This part mainly talks about some AS and Gradle plug-ins with the relevant source code.
Pile insertion and patch generation
The following focuses on the realization of the actual pile insertion.
The code for staking is described in build-system/instant-run-instrumentation, and the actual implementation is to manipulate bytecode directly through ASM.
The bytecode code that ASM manipulates is operated by various mnemonics and is not easy to read.
IncrementalSupportVisitor responsible for operating the original code, is modified to a pile of code
IncrementalChangeVisitor is responsible for generating the changed code to patch, which is actually {original class name}$override
The logic of these two classes in gradle plug-in com. Android. Build. Gradle. Internal. Transforms. InstantRunTransform is invoked. Gradle plug-ins distinguish between classes that have changed, been added, deleted, or not changed.
Look at the first IncrementalSupportVisitor:
The main job is to insert the $change variable and redirect all methods to $override
// IncrementalSupportVisitor.java
/** * Ensures that the class contains a $change field used for referencing the IncrementalChange * dispatcher. * * Also updates package_private visibility to public so we can call into this class from * outside the package. */
@Override
public void visit(int version, int access, String name, String signature, String superName,
String[] interfaces) { visitedClassName = name; visitedSuperName = superName; isInterface = (access & Opcodes.ACC_INTERFACE) ! =0;
int fieldAccess =
isInterface
? Opcodes.ACC_PUBLIC
| Opcodes.ACC_STATIC
| Opcodes.ACC_SYNTHETIC
| Opcodes.ACC_FINAL
: Opcodes.ACC_PUBLIC
| Opcodes.ACC_STATIC
| Opcodes.ACC_VOLATILE
| Opcodes.ACC_SYNTHETIC
| Opcodes.ACC_TRANSIENT;
// when dealing with interfaces, the $change field is an AtomicReference to the CHANGE_TYPE
// since fields in interface must be final. For classes, it's the CHANGE_TYPE directly.
if (isInterface) {
super.visitField(
fieldAccess,
"$change",
getRuntimeTypeName(Type.getType(AtomicReference.class)),
null.null);
} else {
super.visitField(fieldAccess, "$change", getRuntimeTypeName(CHANGE_TYPE), null.null);
}
access = transformClassAccessForInstantRun(access);
super.visit(version, access, name, signature, superName, interfaces);
}
Copy the code
This method is introduced into the public static volatile transient com. Android. View the ir. The runtime. IncrementalChange $change
// Redirection.java
/** * Adds the instructions to do a generic redirection. * <p> * Note that the generated bytecode does not have a direct translation to code, but as an * example, the following code block gets inserted. * <code> * if ($change ! = null) { * $change.access$dispatch($name, new object[] { arg0, ... argsN }) * $anyCodeInsertedbyRestore * } * $originalMethodBody *</code> *@param mv the method visitor to add the instructions to.
* @param change the local variable containing the alternate implementation.
*/
void redirect(GeneratorAdapter mv, int change) {
// code to check if a new implementation of the current class is available.
Label l0 = new Label();
mv.loadLocal(change);
mv.visitJumpInsn(Opcodes.IFNULL, l0);
doRedirect(mv, change);
// Return
if (type == Type.VOID_TYPE) {
mv.pop();
} else {
ByteCodeUtils.unbox(mv, type);
}
mv.returnValue();
// jump label for classes without any new implementation, just invoke the original
// method implementation.
mv.visitLabel(l0);
}
Copy the code
This method insert does the redirection work.
It is also worth noting that {original}$override reassigns methods using the signature of the method. With StringSwitch, the string comparison of the method signature becomes the switch case after the signature hash, which should save a lot of the cost of storing the method signature constants.
The others are not enumerated one by one.
Patch injection
The product is generated, but also need to achieve the product push to the equipment, and modify the parameters reserved before the insertion of piles.
As mentioned earlier, it’s easy to see from the subcontracting of the instant-Run code that it’s C/S architecture. During patch injection, the Server resides in the APP. The client is invoked through AS, and the patch takes effect through pushPatches.
Server since InstantRunContentProvider onCreate lifecycle entrance, the key code is as follows:
private void startServer(a) {
try {
Thread socketServerThread = new Thread(new SocketServerThread());
socketServerThread.start();
} catch (Throwable e) {
// Make sure an exception doesn't cause the rest of the user's
// onCreate() method to be invoked
if (Log.isLoggable(Logging.LOG_TAG, Log.ERROR)) {
Log.e(Logging.LOG_TAG, "Fatal error starting Instant Run server", e); }}}private class SocketServerThread extends Thread {
@Override
public void run(a) {
if (POST_ALIVE_STATUS) {
final Handler handler = new Handler();
Timer timer = new Timer();
TimerTask task =
new TimerTask() {
@Override
public void run(a) {
handler.post(
new Runnable() {
@Override
public void run(a) {
Log.v(
Logging.LOG_TAG,
"Instant Run server still here..."); }}); }}; timer.schedule(task,1.30000L);
}
while (true) {
try {
LocalServerSocket serverSocket = Server.this.serverSocket;
if (serverSocket == null) {
break; // stopped?
}
LocalSocket socket = serverSocket.accept();
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(
Logging.LOG_TAG,
"Received connection from IDE: spawning connection thread");
}
SocketServerReplyThread socketServerReplyThread = new SocketServerReplyThread(
socket);
socketServerReplyThread.run();
if (wrongTokenCount > 50) {
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(
Logging.LOG_TAG,
"Stopping server: too many wrong token connections");
}
Server.this.serverSocket.close();
break; }}catch (Throwable e) {
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(
Logging.LOG_TAG,
"Fatal error accepting connection on local socket",
e);
}
}
}
}
}
Copy the code
Start a socket in a new thread and wait for data from the client. Like other models, validation is first performed. A magicNumber and version are checked upon receiving the data, and then various types of data are passed:
// Server.java
long magic = input.readLong();
if(magic ! = PROTOCOL_IDENTIFIER) { Log.w(Logging.LOG_TAG,"Unrecognized header format " + Long.toHexString(magic));
return;
}
int version = input.readInt();
// Send current protocol version to the IDE so it can decide what to do
output.writeInt(PROTOCOL_VERSION);
if(version ! = PROTOCOL_VERSION) { Log.w( Logging.LOG_TAG,"Mismatched protocol versions; app is "
+ "using version "
+ PROTOCOL_VERSION
+ " and tool is using version "
+ version);
return;
}
while (true) {
int message = input.readInt();
switch (message) {
case MESSAGE_EOF:
Copy the code
InstantRunClient#pushPatches are then called on the AS side to deliver data to the server socket above, which is actually done via adb. The patch file is written to the server side of the socket via bytes. The message type is determined in the above code. The file data stream is read in ApplicationPatch#read and the file data stream is written in ApplicationPatchUtil#write
private static void write(@NonNull DataOutputStream output, @NonNull ApplicationPatch change)
throws IOException {
output.writeUTF(change.path);
byte[] bytes = change.data;
output.writeInt(bytes.length);
output.write(bytes);
}
Copy the code
Patch after the transfer, you could have passed the AbstractPatchesLoaderImpl actual load gives effect to patch the load method.
// AbstractPatchesLoaderImpl
public boolean load(a) {
for (String className : getPatchedClasses()) {
try{ ClassLoader cl = getClass().getClassLoader(); Class<? > aClass = cl.loadClass(className +"$override"); Object o = aClass.newInstance(); Class<? > originalClass = cl.loadClass(className); Field changeField = originalClass.getDeclaredField("$change");
// force the field accessibility as the class might not be "visible"
// from this package.
changeField.setAccessible(true);
Object previous =
originalClass.isInterface()
? patchInterface(changeField, o)
: patchClass(changeField, o);
// If there was a previous change set, mark it as obsolete:
if(previous ! =null) {
Field isObsolete = previous.getClass().getDeclaredField("$obsolete");
if(isObsolete ! =null) {
isObsolete.set(null.true); }}if(logging ! =null && logging.isLoggable(Level.FINE)) {
logging.log(Level.FINE, String.format("patched %s", className)); }}catch (Exception e) {
if(logging ! =null) {
logging.log(
Level.SEVERE,
String.format("Exception while patching %s", className),
e);
}
return false; }}return true;
}
Copy the code
$override = $override = $override = $override = $override = $override; The modified classes are provided by getPatchedClasses(), the actual implementation signature of which is specified by Gradle.
Once $override replaces the new change class, the change takes effect. Look again at the analysis in Principles.
In the next article, I will try to implement my own Hotfix framework step by step using the principles of Instant Run.