takeaway

This is the second article of my analysis on didi Open Source library Booster. The first article is titled “Didi Open Source library Booster: Architecture operation and source code Analysis”.

I want to divide “performance optimization”, “Lint” and “resource compression” into three explicit articles according to Booster. However, a close look at the source code of the ClassTransformer subclass shows that a single article is enough.

Since the ClassTransformer subclass is the main entry point for execution, I read it in alphabetical order with the suffix of the ‘-transform-‘ module name. Most of the logic processing code, which requires a basic understanding of THE ASM API is easy to comb.

The module corresponding to ‘-instrument-‘ is responsible for providing the implementation class of specific hook methods, which are mainly static methods, and the class name is highly readable: ‘ShadowXXX’, you can see which class and method hook is at a glance.

So the main content of this article is to describe the idea behind the hook method logic, what to solve, for the specific code involving the use of ASM bytecode operation, is not the focus of this article.

The body of the

Before I start analyzing the instructions, I put together the following illustration

It shows the main work of the Transform and instrument modules, as well as their respective responsibilities.

Thread

1.transformInvokeVirtual:

  • The instruction was detected as a callThread.start()Methods – >
// Push String constant value "\u200B"+ class name to opcode stack
method.instructions.insertBefore(this, LdcInsnNode(makeThreadName(klass.className)))
/ / insert method call instruction to ShadowThread. SetThreadName ()
method.instructions.insertBefore(this, MethodInsnNode(Opcodes.INVOKESTATIC, SHADOW_THREAD, "setThreadName"."(Ljava/lang/Thread; Ljava/lang/String;) Ljava/lang/Thread;".false))
/ / set the onwer
this.owner = THREAD
Copy the code
  • The instruction was detected as a callThread.setName(String name)Methods – >
// Push String constant value "\u200B"+ class name to opcode stack
method.instructions.insertBefore(this, LdcInsnNode(makeThreadName(klass.className)))
/ / insert method call instruction to ShadowThread. MakeThreadName (), on a constant value as the method of the first parameter
method.instructions.insertBefore(this, MethodInsnNode(Opcodes.INVOKESTATIC, SHADOW_THREAD, "makeThreadName"."(Ljava/lang/String; Ljava/lang/String;) Ljava/lang/String;".false))
/ / set the onwer
this.owner = THREAD
Copy the code

2.transformInvokeStatic:

  • forExecutorAll involved in creatingExecutorServiceThe static method for hook
EXECUTORS -> {
    when (this.name) {
        "defaultThreadFactory"- > {val r = this.desc.lastIndexOf(') ')
            val desc = "${this.desc.substring(0, r)}Ljava/lang/String;${this.desc.substring(r)}"
            logger.println("*${this.owner}.${this.name}${this.desc}= >$SHADOW_EXECUTORS.${this.name}$desc: ${klass.name}.${method.name}${method.desc}")
            this.owner = SHADOW_EXECUTORS
            this.desc = desc
            method.instructions.insertBefore(this, LdcInsnNode(makeThreadName(klass.className)))
        }
        "newCachedThreadPool"."newFixedThreadPool"."newSingleThreadExecutor"."newSingleThreadScheduledExecutor"."newScheduledThreadPool"- > {val r = this.desc.lastIndexOf(') ')
            val name = this.name.replace("new"."newOptimized")
            val desc = "${this.desc.substring(0, r)}Ljava/lang/String;${this.desc.substring(r)}"
            logger.println("*${this.owner}.${this.name}${this.desc}= >$SHADOW_EXECUTORS.$name$desc: ${klass.name}.${method.name}${method.desc}")
            this.owner = SHADOW_EXECUTORS
            this.name = name
            this.desc = desc
            method.instructions.insertBefore(this, LdcInsnNode(makeThreadName(klass.className)))
        }
    }
}
Copy the code

The Hook method provided by ShadowExecutor uses NamedThreadFactory to create the Fatory class of the created thread, ensuring that the name, isDaemon, and NORM_PRIORITY of the created thread are clearly arranged.

NamedThreadFactory:

@Override
    public Thread newThread(final Runnable r) {
        if (null= =this.factory) {
            final Thread t = new Thread(this.group, r, this.name + "#" + this.counter.getAndIncrement(), 0);

            if (t.isDaemon()) {
                t.setDaemon(false);
            }

            if(t.getPriority() ! = Thread.NORM_PRIORITY) { t.setPriority(Thread.NORM_PRIORITY); }return t;
        }

        return setThreadName(this.factory.newThread(r), this.name);
    }
Copy the code

3.transformInvokeSpecial

  • forThreadAll constructors of the hook.
when (this.desc) {
    "()V"."(Ljava/lang/Runnable;) V"."(Ljava/lang/ThreadGroup; Ljava/lang/Runnable;) V" -> {
        method.instructions.insertBefore(this, LdcInsnNode(makeThreadName(klass.className)))
        val r = this.desc.lastIndexOf(') ')
        val desc = "${this.desc.substring(0, r)}Ljava/lang/String;${this.desc.substring(r)}"
        logger.println("+$SHADOW_THREAD.makeThreadName(Ljava/lang/String; Ljava/lang/String;) = >${this.owner}.${this.name}${this.desc}: ${klass.name}.${method.name}${method.desc}")
        logger.println("*${this.owner}.${this.name}${this.desc}= >${this.owner}.${this.name}$desc: ${klass.name}.${method.name}${method.desc}")
        this.desc = desc
    }
    "(Ljava/lang/String;) V"."(Ljava/lang/ThreadGroup; Ljava/lang/String;) V"."(Ljava/lang/Runnable; Ljava/lang/String;) V"."(Ljava/lang/ThreadGroup; Ljava/lang/Runnable; Ljava/lang/String;) V" -> {
        method.instructions.insertBefore(this, LdcInsnNode(makeThreadName(klass.className)))
        method.instructions.insertBefore(this, MethodInsnNode(Opcodes.INVOKESTATIC, SHADOW_THREAD, "makeThreadName"."(Ljava/lang/String; Ljava/lang/String;) Ljava/lang/String;".false))
        logger.println("+$SHADOW_THREAD.makeThreadName(Ljava/lang/String; Ljava/lang/String;) = >${this.owner}.${this.name}${this.desc}: ${klass.name}.${method.name}${method.desc}")}"(Ljava/lang/ThreadGroup; Ljava/lang/Runnable; Ljava/lang/String; J)V" -> {
        method.instructions.insertBefore(this, InsnNode(Opcodes.POP2)) // discard the last argument: stackSize
        method.instructions.insertBefore(this, LdcInsnNode(makeThreadName(klass.className)))
        method.instructions.insertBefore(this, MethodInsnNode(Opcodes.INVOKESTATIC, SHADOW_THREAD, "makeThreadName"."(Ljava/lang/String; Ljava/lang/String;) Ljava/lang/String;".false))
        logger.println("+$SHADOW_THREAD.makeThreadName(Ljava/lang/String; Ljava/lang/String;) = >${this.owner}.${this.name}${this.desc}: ${klass.name}.${method.name}${method.desc}")
        this.desc = "(Ljava/lang/ThreadGroup; Ljava/lang/Runnable; Ljava/lang/String;) V"}}Copy the code

ActivityThread

Get a subclass of custom Handler, H, and replace the proxy with ActivityThreadCallback in the custom instrument. ActivityThreadCallback follows a similar pattern to the CaughtCallback class, except that it catches different types and numbers of exceptions.

@Override
    public final boolean handleMessage(final Message msg) {
        try {
            this.mHandler.handleMessage(msg);
        } catch (final NullPointerException e) {
            if (hasStackTraceElement(e, ASSET_MANAGER_GET_RESOURCE_VALUE, LOADED_APK_GET_ASSETS)) {
                abort(e);
            }
            rethrowIfNotCausedBySystem(e);
        } catch (final SecurityException
                | IllegalArgumentException
                | AndroidRuntimeException
                | WindowManager.BadTokenException e) {
            rethrowIfNotCausedBySystem(e);
        } catch (final Resources.NotFoundException e) {
            rethrowIfNotCausedBySystem(e);
            abort(e);
        } catch (final RuntimeException e) {
            final Throwable cause = e.getCause();
            if (((Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) && isCausedBy(cause, DeadSystemException.class))
                    || (isCausedBy(cause, NullPointerException.class) && hasStackTraceElement(e, LOADED_APK_GET_ASSETS))) {
                abort(e);
            }
            rethrowIfNotCausedBySystem(e);
        } catch (final Error e) {
            rethrowIfNotCausedBySystem(e);
            abort(e);
        }

        return true;
    }
Copy the code

finalizer-watchdog-daemon

In the Application of attachBaseContext before () method, add the hook method FinalizerWatchdogDaemonKiller. Kill ()

 valmethod = klass.methods? .find {"${it.name}${it.desc}"= ="attachBaseContext(Landroid/content/Context;) V"
} ?: klass.defaultAttachBaseContext

method.instructions?.findAll(RETURN, ATHROW)?.forEach {
    method.instructions?.insertBefore(it, MethodInsnNode(INVOKESTATIC, FINALIZER_WATCHDOG_DAEMON_KILLER, "kill"."()V".false))
    logger.println("+$FINALIZER_WATCHDOG_DAEMON_KILLER.kill()V before @${if (it.opcode == ATHROW) "athrow" else "return"}: ${klass.name}.${method.name}${method.desc} ")}Copy the code

FinalizerWatchdogDaemonKiller responsibilities:

  • Try up to 10 times to find the class namejava.lang.Daemons$FinalizerWatchdogDaemonThe thread.
  • Use reflection to get its singletonINSTANCEObject to setthreadProperty is null, called on failurestop()methods

For the reason of such treatment, please refer to the solution proposed by Didi technology.

Logcat

Main role: in building production packages (! Debuggable), all log printing code implementation masking. For three classes: android.util.Log, java.lang.Throwable, java.lang.System.

  • Any instruction that calls the v, D, I, W,e, WTF,println methods of Log replaces the empty implementation method corresponding to ShadowLog class with owner.

  • Throwable. Any call printStackTrace () method of instruction, instead of static call ShadowThrowable static empty method printStackTrace ().

public final class ShadowThrowable {

    public static void printStackTrace(final Throwable t) {}}Copy the code
  • Any callsSystem.out.System.errStatic get property directive, modifiedownerInstead ofShadowSystem, the correspondingOut, errObject is an empty implementation.
public final class ShadowSystem {

    public static final PrintStream out = new PrintStream(new OutputStream() {
        @Override
        public void write(final int b) {}});public static final PrintStream err = out;

    private ShadowSystem(a) {}}Copy the code

MediaPlayer

  • For method directives that call the mediaPlayer.create () method or the new MediaPlayer() constructor, change the owner to point to the ShadowMediaPlayer class.

  • ShadowMediaPlayer class replaces CaughtCallback with the reflection, HookMediaPlayer object’s mEventHandler property.

private static MediaPlayer workaround(final MediaPlayer player) {
        try {
            final Handler handler = getEventHandler(player);
            if (null== handler || ! setFieldValue(handler,"mCallback", new CaughtCallback(handler))) {
                Log.i(TAG, "Hook MediaPlayer.mEventHandler.mCallback failed"); }}catch (final Throwable t) {
            Log.e(TAG, "Hook MediaPlayer.mEventHandler.mCallback failed", t);
        }

        return player;
    }
Copy the code

The CaughtCallback class is defined in the ‘booster-Android-instrument’ module. Take care of the Handler object passed in by the broker, add a try-catch block to its handlerMessage(Message MSG) method, and catch RuntimeException for fixbug effect. It is mainly used to replace the Handler object inside the system API class.

public class CaughtCallback implements Handler.Callback {

    private final Handler mHandler;
    
    public CaughtCallback(final Handler handler) {
        this.mHandler = handler; // Proxy Handler object
    }

    @Override
    public boolean handleMessage(final Message msg) {
        try {
            this.mHandler.handleMessage(msg);
        } catch (final RuntimeException e) {
            // ignore
        }
        return true; }}Copy the code

res-check

For subclasses of Application, the insert directive calls the resChecker.checkres (Application Application) method after the super-.attachBasecontext () statement is called

The checkRes() method checks application.getassets (), application.getResources ()

public class ResChecker {

    public static void checkRes(final Application app) {
        if (null == app.getAssets() || null == app.getResources()) {
            final int pid = Process.myPid();
            Log.w(TAG, "Process " + pid + " is going to be killed");
            Process.killProcess(pid);
            System.exit(10); }}}Copy the code

shared-preferences

  • For editor.mit (), if its return value is not used, change to shadoweditor.apply (Editor).

  • For the editor.apply () method, call shadoweditor.apply (Editor) asynchronously instead.

  • Shadoweditor.apply (Editor) logic: on the main thread, call asynchronously, otherwise call commit() directly.

public class ShadowEditor {

    public static void apply(final SharedPreferences.Editor editor) {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
                @Override
                public void run() { editor.commit(); }}); }else{ editor.commit(); }}}Copy the code

shrink

1. Delete R$*. Class and r.class. Ignore classes that exclude some package paths by default. Support configuration properties booster. The transform. The shrink. Ignores, add to exclude class. 2. Delete useless constant properties

Toast

Replace the toast.show () method with shadowtoast.show (),

public static void show(final Toast toast) {
        if (Build.VERSION.SDK_INT == 25) {
            workaround(toast).show();
        } else{ toast.show(); }}Copy the code

Only for SDK 25:

    private static Toast workaround(final Toast toast) {
        final Object tn = getFieldValue(toast, "mTN");
        if (null == tn) {
            Log.w(TAG, "Field mTN of " + toast + " is null");
            return toast;
        }

        final Object handler = getFieldValue(tn, "mHandler");
        if (handler instanceof Handler) {
            if (setFieldValue(handler, "mCallback", new CaughtCallback((Handler) handler))) {
                return toast;
            }
        }

        final Object show = getFieldValue(tn, "mShow");
        if (show instanceof Runnable) {
            if (setFieldValue(tn, "mShow", new CaughtRunnable((Runnable) show))) {
                return toast;
            }
        }

        Log.w(TAG, "Neither field mHandler nor mShow of " + tn + " is accessible");
        return toast;
    }
Copy the code
  • Reflection to obtainToast.mTN.mHandlerObject, delegate toCaughtCallbackTo deal with
  • Reflection to obtainToast.mTn.mShowObject, delegate toCaughtRunnableTo deal with

Usage

According to the set of booster. The transform. The usage. The apis attributes, when building the print matching API description method. It will not print if it is not configured.

WebView

In the Application in super. The onCreate () is called before, first call ShadowWebView. PreloadWebView () method.

Android. Its. WebViewFactory. GetProvider () – > WebViewFactoryProvider. StartYourEngines preload () implementation

summary

In fact, the above description is only the surface of things, a lot of internal knowledge points, I still need to digest:

  • Why do you want toapplicationtheattachBaseContext()andonCreate()Before adding method? When should I add it
  • SharedPreferenced.EditorthecommitandapplyUnderlying logic?
  • FinalizerWatchdogDaemonHow does the finalizer method listen for objects
  • Shrink resource method details
  • The application of Kotlin’s method extension requires a deeper understanding through practice
  • Source code used in the design pattern

These need to continue to study the knowledge points of other aspects to supplement. For now, Booster’s analysis will be suspended until the official roadmap is supplemented.