Before, I shared a lot of dACHang, many students felt that dachang output more theoretical knowledge, lack of practice.

Then this article, we will introduce a large factory library, this library can help you solve some problems.

Today’s protagonist: beginners Xiao Zhang, senior research and development of old sheep.

Bugs in the tripartite library

One day QA gave A bug feedback to Zhang before going online. The application crashed when started. Zhang didn’t panic at all.

Want to think, empty pointer is better repair, big deal judge empty defense once, hence answer: this problem give me, repair immediately.

Based on the stack, the culprit of the null pointer was found.

Suddenly, Chang was shocked. The null pointer was a tripartite library that had failed to get the user clipboard during initialization.

How can this be solved?

I thought I’d call an air defense and get it over with.

After all, you’re the one who faked it, and you’re gonna have to fix it in tears. Let’s do a simulation.

Public class Tools {public static String getClipBoardStr(Context Context) {ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); ClipData primaryClip = clipboardManager.getPrimaryClip(); // NPE ClipData.Item itemAt = primaryClip.getItemAt(0); if (itemAt == null) { return ""; } CharSequence text = itemAt.getText(); if (text == null) { return ""; } return text.toString(); }}Copy the code

Let’s write a button to trigger it:

Sure enough collapse happened, null Pointers occur in clipboardManager. GetPrimaryClip (), when no duplicate content on your mobile phone, getPrimaryClip returns null.

It will soon be online, but this problem is not impossible to fix, according to my experience, most system services can be hook, hook off ClipboradManager related methods, to ensure that the return of getPrimaryClip is not null.

So I looked at a few points:

public @Nullable ClipData getPrimaryClip() { try { return mService.getPrimaryClip(mContext.getOpPackageName(), mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); }}Copy the code

This mService is initialized as:

mService = IClipboard.Stub.asInterface(
                ServiceManager.getServiceOrThrow(Context.CLIPBOARD_SERVICE));
Copy the code

In this way, we can hook it already. Can we construct ClipData by ourselves?

public ClipData(CharSequence label, String[] mimeTypes, Item item) {}
Copy the code

Well, the hook idea is basically feasible.

Zhang secretly pleased, thanks to meet me ah, fortunately I solid strength.

At this time, senior research and development of the old sheep came over to ask, will soon be online, what do you do?

Zhang gushed about the problems he encountered and his solutions, expecting that the old sheep would pat him on the shoulder and say “it’s a good thing you met him” to show his recognition.

The old sheep said:

GetPrimaryClip returns a null pointer, so why not call setPrimaryClip earlier?

Boon? Oh my god… Take a look at the source code:

#ClipboardManager public void setPrimaryClip(@NonNull ClipData clip) { try { Preconditions.checkNotNull(clip); clip.prepareToLeaveProcess(true); mService.setPrimaryClip(clip, mContext.getOpPackageName(), mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); }}Copy the code

There is a way…

Give it a try.

Added a line:

ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
clipboardManager.setPrimaryClip(new ClipData("bugfix", new String[]{"text/plain"}, new ClipData.Item("")));

Copy the code

It was no longer crashing.

Then the old sheep said:

Think about it. Suppose there was a fatal bug in the tripartite library, and you couldn’t find the right hook point, what would you do? Let me know when you’re ready.

Fatal bug, failed to find suitable hook point?

Code under simulation:

public class Tools { public static void evilCode() { int a = 1 / 0; } public static String getClipBoardStr(Context context) { evilCode(); ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); ClipData primaryClip = clipboardManager.getPrimaryClip(); ClipData.Item itemAt = primaryClip.getItemAt(0); if (itemAt == null) { return ""; } CharSequence text = itemAt.getText(); if (text == null) { return ""; } return text.toString(); }}Copy the code

Let’s say getClipBoardStr internally calls a line of evilCode and crashes.

At first glance, the evilCode method is simple, but what about in a tripartite library?

Zhang, puzzled, suddenly had an idea:

Is the old sheep want to test my ability to push, let me not blind hook other people’s code, this kind of problem of course find the third party library to fix, and then give a new version.

So I ran to the old sheep and told him that I had thought of this kind of problem. We should timely promote the third party library to solve it, and then we can upgrade the version.

After hearing this, the old sheep, well, really want to find them, but if it is before the online encounter, promotion is certainly too late, even if they immediately give you a new version, direct upgrade risk is relatively large.

Then the old sheep said:

I see you are familiar with the reflection hook point. In fact, there is a kind of hook that is more useful and more stable.

It’s called a bytecode hook.

How do you say?

When our code is packaged, it goes through the following steps:

.java -> .class -> dex -> apk

The evil methods of the above class are actually bytecode from the class file point of view.

Suppose we do this during compilation:

.java ->.class -> get tools. class and fix methods in it -> dex -> apk

This time, in fact, is also provided during the build process, which is the legendary Transform phase (we will not discuss the changes after AGP 7, there is a corresponding time).

Zhang: I know. How do I modify the Tools.class file?

The old sheep said, look at my blog:

Android advanced path: ASM modify bytecode, so learn the right!

Then again, if you’re going to have this pain point, other developers are going to have this pain point, too.

What should you think at this point?

Zhang: Someone must have built a good wheel.

Old Sheep: Well, 99% of the time, the wheel must be built, the remaining 1%, that’s your chance.

The Lightweight AOP framework Lancet emerged

Are you hungry? A framework called The Lancet has been open source for a long time.

Github.com/eleme/lance…

The framework allows you to modify method bytecodes without knowing them.

Substitute for the idea we just had:

Java ->. Class -> Lancet get Tools. Class, fix evilCode method -> dex -> apk

Zhang: How can we use the Lancet to modify our evilCode method?

The introduction of the framework

In the root directory of the project add:

Classpath 'me. Ele: lancet - plugin: 1.0.6'Copy the code

Add dependencies and apply plugins to module build.gradle:

Implementation 'id: 0 0 0 0 0 0 0 0Copy the code

Begin to use

Then, we do one thing, put the evilCode method inside Tools:

public static void evilCode() {
    int a = 1 / 0;
}
Copy the code

Let’s get rid of this code and make it an empty method.

We write code:

package com.imooc.blogdemo.blog04;

import me.ele.lancet.base.annotations.Insert;
import me.ele.lancet.base.annotations.TargetClass;

public class ToolsLancet {

    @TargetClass("com.imooc.blogdemo.blog04.Tools")
    @Insert("evilCode")
    public static void evilCode() {

    }

}
Copy the code

We’ll write a new method, make sure it’s empty, and we’ll be done calling the old evilCode.

Among them:

  • TargetClass annotation: Identifies the name of the class you want to modify;
  • Insert: indicates that you need to inject the following code into the evilCode method
  • The following method declarations need to be the same as the original method, and if there are any parameters, the parameters must be the same (method names and parameter names need not be the same).

Then we pack up and see what happens behind the scenes.

After the packaging is complete, we decompile and look at tools.class

public class Tools {	
   //... 
    public static void evilCode() {
        Tools._lancet.com_imooc_blogdemo_blog04_ToolsLancet_evilCode();
    }

    private static void evilCode$___twin___() {
        int a = 1 / 0;
    }

    private static class _lancet {
        private _lancet() {
        }

        @TargetClass("com.imooc.blogdemo.blog04.Tools")
        @Insert("evilCode")
        static void com_imooc_blogdemo_blog04_ToolsLancet_evilCode() {
        }
    }
}
Copy the code

As you can see, the validation in the original evilCode method has been replaced with a generated method call, which is very similar to the one we wrote and is empty.

The original evilCode logic is placed in an evilCode$___twin___() method that unfortunately has nowhere to call.

The original evilCode logic becomes an empty method.

We can outline the principles:

The Lancet will transfer the method calls we indicated need to be modified into a temporary method that you can interpret as being basically consistent with the method logic we wrote.

Then the original logic of the method is also extracted into a new method for use.

Xiao Zhang: That’s really amazing. When will we use the original method?

Old Yang: Most of the time, the original logic is only a very low probability problem, such as sending a request, the error will only occur in the case of timeout, you can not remove the logic of the other person roughly, you may want to add a try catch and give a hint or something.

At this point you can change it like this:

package com.imooc.blogdemo.blog04; import me.ele.lancet.base.Origin; import me.ele.lancet.base.annotations.Insert; import me.ele.lancet.base.annotations.TargetClass; public class ToolsLancet { @TargetClass("com.imooc.blogdemo.blog04.Tools") @Insert("evilCode") public static void evilCode() { try { Origin.callVoid(); } catch (Exception e) { e.printStackTrace(); }}}Copy the code

Let’s look at the decompiled code again:

public class Tools { public static void evilCode() { Tools._lancet.com_imooc_blogdemo_blog04_ToolsLancet_evilCode(); } private static void evilCode$___twin___() { int a = 1 / 0; } private static class _lancet { @TargetClass("com.imooc.blogdemo.blog04.Tools") @Insert("evilCode") static void com_imooc_blogdemo_blog04_ToolsLancet_evilCode() { try { Tools.evilCode$___twin___(); } catch (Exception var1) { var1.printStackTrace(); }}}}Copy the code

See, as expected, the relay method calls the original method inside, and then it has a try-catch wrapped around it.

Is it powerful and more stable than run-time reflection hooks? It’s just like the code you wrote, but with the class changed directly.

Zhang: So the shear board crash I encountered this morning could also have been tried and caught by The Lancet.

Old Yang: Yes, I can draw a conclusion from one point to another. Of course, it also reflects the power of bytecode hook from the side. There is almost no need to find any hook point, as long as you have a method, you can interfere.

In addition, what I’ve shown you is the most basic API, so go ahead and look at other uses of the Lancet.

Xiao Zhang: Ok, there you go.

A new problem arises

After a few days, the project suddenly encountered another problem:

Users are not allowed to read the clipboard without authorization. Otherwise, the clipboard is considered as non-compliance.

When Xiao Zhang heard the task, his brain worked fast:

This API reads the behavior of the clipboard:

clipboardManager.getPrimaryClip();
Copy the code

Search for calls under the project and modify them one by one.

Not to mention whether the search can be complete, there are certainly three libraries, in addition, how to control the subsequent addition of the code?

Before learning other lancet, can modify the tripartite library code, but I also can’t contain clipboardManager. GetPrimaryClip method all listed, each bytecode modification?

Still can’t solve the follow-up addition, already can guarantee all search out ah.

Finally, he said, “Don’t let me do it. Don’t let me do it. It’s probably a pit.”

At this time, the old sheep said: This is simple, xiao Zhang is familiar with, he can do it.

Zhang: I…

Think again, search out anyway, one by one modification is impossible.

Then address it at source:

The system must use the framework system process to determine whether to read the clipboard.

So we just need to put:

clipboardManager.getPrimaryClip
	IClipboard.getPrimaryClip(mContext.getOpPackageName(), mContext.getUserId());
Copy the code

We hook the internal logic, replace the implementation of IClipBoard, and then cut to our own logic.

I see, this is just the hook of system service I thought before. No wonder Lao Yang arranged it for me. I told him about it.

As a result… I went into writing mode…

The code is omitted here. (Yes, but it’s not the main topic of this article.)

Just finished testing Android 10.0, ready to go through the various versions of the source code for the adaptation, the old sheep walked in.

It’s been two hours and you still haven’t done it?

Xiao Zhang: Two hours? To have you.

Old Goat: I asked you to look at the other APIS in the Lancet.

This is called the Lancet as a question to give points. Do you know that? Like:

Public class ToolsLancet {public static Boolean isAuth = true; @TargetClass("android.content.ClipboardManager") @Proxy("getPrimaryClip") public ClipData getPrimaryClip() { if (isAuth)  { return (ClipData) Origin.call(); } null return new ClipData(" unauthorized ", new String[]{"text/plain"}, new clipdata.item (""));  }}Copy the code

Xiao zhang: this not line, android. Content. ClipboardManager class is a system, is not what we write, don’t have the class at packing stage.

Old Sheep: OF course I know. Please look carefully. What’s the difference between the annotations used this time and last time?

This time with:

  • @proxy: means Proxy, will Proxy ClipboardManager. GetPrimaryClip to our method.

Let’s decompile:

Original call:

public static String getClipBoardStr(Context context) {
    ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
    ClipData primaryClip = clipboardManager.getPrimaryClip();
    ClipData.Item itemAt = primaryClip.getItemAt(0);
    if (itemAt == null) {
        return "";
    }
    CharSequence text = itemAt.getText();
    if (text == null) {
        return "";
    }
    return text.toString();
}
Copy the code

Decompiled calls:

public class Tools { public static String getClipBoardStr(Context context) { ClipboardManager clipboardManager = (ClipboardManager)context.getSystemService("clipboard"); ClipData primaryClip = Tools._lancet.com_imooc_blogdemo_blog04_ToolsLancet_getPrimaryClip(clipboardManager); Item itemAt = primaryClip.getItemAt(0); if (itemAt == null) { return ""; } else { CharSequence text = itemAt.getText(); return text == null ? "" : text.toString(); } } private static class _lancet { @TargetClass("android.content.ClipboardManager") @Proxy("getPrimaryClip") static ClipData com_imooc_blogdemo_blog04_ToolsLancet_getPrimaryClip(ClipboardManager var0) { return ToolsLancet.isAuth ? Var0.getprimaryclip () : new ClipData(" unauthorized ", new String[]{"text/plain"}, new Item("")); }}}Copy the code

See no, clipboardManager getPrimaryClip () method into Tools._lancet.com _imooc_blogdemo_blog04_ToolsLancet_getPrimaryClip, To our hook implementation.

Get it this time:

  1. The Lancet uses the @insert directive for our own in-class methods;
  2. When the system calls, we can use the @proxy directive for the calling function to transfer it to the transfer function;

Okay, The Lancet has some more apis, so you can go down there and take a look.

The end

Finally, it’s over. Everyone exits the dialogue scene between Xiao Zhang and the old sheep.

In fact, bytecode hook is more powerful in the Android development process, than our traditional hook point (singleton, static variables), and then reflect the way is too convenient, there is a biggest advantage is stable.

Of course, the premise of Lancet Hook is to know exactly how to call a method. If you want to hook all the calls to a class, then it’s a bit more laborious to write and may not be as convenient as dynamic proxies.

All right, anyway:

There was a guy who went to an interview and was asked,

How to converge the creation of the three-party library surface thread pool?

Do you have an idea?

My name is Hongyang, and I try to make complex knowledge into life. I write articles by way of q&A and guidance, so that everyone can read them in one breath. I hope you have gained something from them.

Say goodbye! See you next time!

Welcome to follow my public account “Hongyang”, you can receive updates as soon as possible.