Practice Sample to Run a Peers' Practice Notebook 3rd Issue

Practice Sample to Run A Peers’ Practice Notebook 3rd Issue #

I didn’t expect that my previous exercise reflections would be recognized by the teacher. It seems that I have to practice harder. Today, I’m going to practice the samples of Chapter 22, Chapter 27, and ASM.

Chapter 22

Trying to use the Facebook ReDex library to optimize our installation package.

Preparation

First, download ReDex:

git clone https://github.com/facebook/redex.git
cd redex

Then install it:

autoreconf -ivf && ./configure && make -j4
sudo make install

During the installation, an error like the following image was reported:

It means that Boost is not installed, so execute the following command to install it:

brew install boost jsoncpp

After installing Boost, wait for about ten minutes to install ReDex.

Next, compile our sample to get the package information.

You can see that there are three Dex files and the APK size is 13.7MB.

Optimize with ReDex command

To make the process clearer, you can output the log of ReDex.

export TRACE=2

To remove Debuginfo, execute the following command in the project root directory:

redex --sign -s ReDexSample/keystore/debug.keystore -a androiddebugkey -p android -c redex-test/stripdebuginfo.config -P ReDexSample/proguard-rules.pro  -o redex-test/strip_output.apk ReDexSample/build/outputs/apk/debug/ReDexSample-debug.apk

The long command above can actually be broken down into several parts:

  • --sign sign information
  • -s (keystore) path to the signing file
  • -a (keyalias) alias used for signing
  • -p (keypass) password for signing
  • -c specify the path to the ReDex configuration file
  • -P path to the ProGuard rules file
  • -o output file path
  • Finally, the path to the APK file to be processed

However, when I used it, I encountered the problem shown in the following image:

Here, “Zipalign” was not found, so we need to configure the root directory path of the Android SDK and add it in front of the original command:

ANDROID_SDK=/path/to/android/sdk redex [... arguments ...]

The result is as follows:

The actual optimization effect is that the original Debug package is 14.21MB, and after removing Debuginfo, it becomes 12.91MB. The effect is not bad. The removed content includes some debugging information and stack line numbers.

However, the teacher added -keepattributes SourceFile,LineNumberTable in the proguard-rules.pro of the Sample to retain the line number information.

So after processing the package, when it is installed and goes to the homepage, you can still see the stack information with line numbers.

Method of Dex repackaging

redex --sign -s ReDexSample/keystore/debug.keystore -a androiddebugkey -p android -c redex-test/interdex.config -P ReDexSample/proguard-rules.pro  -o redex-test/interdex_output.apk ReDexSample/build/outputs/apk/debug/ReDexSample-debug.apk

The command above is the same as before, except that -c uses the interdex.config configuration file.

Output information:

The optimization effect is that the original Debug package is 14.21MB with 3 DEX files, and after optimization, it becomes 13.34MB with 2 DEX files.

According to the teacher’s introduction, if your application has more than 4 DEX files, this optimization can reduce the volume by at least 10%. It seems that the effect is great. As for other issues, such as using ReDex in the Windows environment, you can refer to the documentation of ReDex.

Chapter 27

Implementing instrumentation using AspectJ.

The effect is the same as Chapter 07, except that Chapter 07 uses ASM for implementation, while this time AspectJ is used. Both ASM and AspectJ are Java bytecode processing frameworks. AspectJ is easier to use, and the same functionality can be achieved with just a few lines of code below, but ASM is more efficient and flexible than AspectJ.

AspectJ implementation code:

@Aspect
public class TraceTagAspectj {

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    @Before("execution(* **(..))")
    public void before(JoinPoint joinPoint) {
        Trace.beginSection(joinPoint.getSignature().toString());
    }

    /**
     * hook method when it's called out.
     */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    @After("execution(* **(..))")
    public void after() {
        Trace.endSection();
    }
}

A brief explanation of the above code:

  • @Aspect: At compile time, AspectJ will search for classes annotated with @Aspect and execute our AOP implementation.

  • @Before: Can be simply understood as before the method is executed.

  • @After: Can be simply understood as after the method is executed.

  • execution: Method execution.

  • * **(..): The first asterisk represents any return type, the second asterisk represents any class, the third represents any method, and the parentheses represent unlimited method parameters. The asterisk and the contents inside the parentheses can be replaced with specific values, such as String TestClass.test(String).

Knowing the meanings of the relevant annotations, the meaning of the implementation code is to insert corresponding specified operations before and after the execution of all methods. The comparison of effects is as follows:

-

Next, add a try-catch to the onResume method of MainActivity.

@Aspect
public class TryCatchAspect {
    
    @Pointcut("execution(* com.sample.systrace.MainActivity.onResume())") // <- specify the class and method
    public void methodTryCatch() {
    }

    @Around("methodTryCatch()")
    public void aroundTryJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
       
         // try catch
         try {
             joinPoint.proceed(); // <- call the original method
         } catch (Exception e) {
              e.printStackTrace();
         }
    }
}

Two new annotations are used above:

  • @Around: Used to replace the previous code, and joinPoint.proceed() can be used to call the original method.
  • @Pointcut: Specifies a pointcut.

The implementation is to specify a pointcut and wrap it with a try-catch by using the idea of replacing the original method.

The comparison of effects is as follows:

-

Of course, AspectJ has many other uses. The Sample includes “AspectJ程序设计指南” (AspectJ Program Design Guide) to help us understand and learn AspectJ in detail.

Chapter-ASM

Sample uses ASM to implement the timing statistics of method execution and replace all new Thread operations in the project.

  • To run the project, first comment out the apply plugin: 'com.geektime.asm-plugin' in ASMSample’s build.gradle and classpath ("com.geektime.asm:asm-gradle-plugin:1.0") { changing = true } in the root build.gradle file.

  • Run gradle task ":asm-gradle-plugin:buildAndPublishToLocalMaven" to compile the plugin. The compiled plugin will be in the local .m2\repository directory.

  • Open the commented content in the first step and it can be run.

The general process is to first use Transform to iterate through all the files, then use ASM’s visitMethod to iterate through all the methods, and finally use AdviceAdapter to implement the final bytecode modification. You can refer to the code and the article “练习Sample跑起来 | ASM插桩强化练习” (Practice Sample Running | Enhanced Exercise of ASM Instrumentation) for specific implementation details.

Comparison of effects:

-

Here are two exercises:

  1. Add try-catch to a specific method.

Here, I added try-catch to the mm method in MainActivity. The implementation is simple, just directly modify ASMCode’s TraceMethodAdapter.

public static class TraceMethodAdapter extends AdviceAdapter {

    private final String methodName;
    private final String className;
    private final Label tryStart = new Label();
    private final Label tryEnd = new Label();
    private final Label catchStart = new Label();
    private final Label catchEnd = new Label();

    protected TraceMethodAdapter(int api, MethodVisitor mv, int access, String name, String desc, String className) {
        super(api, mv, access, name, desc);
        this.className = className;
        this.methodName = name;
    }

    @Override
    protected void onMethodEnter() {
        if (className.equals("com/sample/asm/MainActivity") && methodName.equals("mm")) {
            mv.visitTryCatchBlock(tryStart, tryEnd, catchStart, "java/lang/Exception");
            mv.visitLabel(tryStart);
        }
    }

    @Override
    protected void onMethodExit(int opcode) {
        if (className.equals("com/sample/asm/MainActivity") && methodName.equals("mm")) {
            mv.visitLabel(tryEnd);
            mv.visitJumpInsn(GOTO, catchEnd);
            mv.visitLabel(catchStart);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/RuntimeException", "printStackTrace", "()V", false);
            mv.visitInsn(Opcodes.RETURN);
            mv.visitLabel(catchEnd);
        }
    }

The visitTryCatchBlock method: the first three parameters are all Label instances, where the first two represent the range of the try block, the third is the start position of the catch block, and the fourth parameter is the exception type. I won’t go into detail about other methods and parameters, you can refer to the ASM documentation for specific information.

Implementing something similar to AspectJ, inserting our code at the start and end of method execution.

I won’t take a screenshot of the effect. The code is as follows:

public void mm() {
    try {
        A a = new A(new B(2));
    } catch (Exception e) {
        e.printStackTrace();
    }
}
  1. Find out who is obtaining the IMEI in the code.

This is even simpler, just find out who is using the getDeviceId method of TelephonyManager. The answer is in the Sample.

public class IMEIMethodAdapter extends AdviceAdapter {

    private final String methodName;
    private final String className;

    protected IMEIMethodAdapter(int api, MethodVisitor mv, int access, String name, String desc, String className) {
        super(api, mv, access, name, desc);
        this.className = className;
        this.methodName = name;
    }

    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
        super.visitMethodInsn(opcode, owner, name, desc, itf);

        if (owner.equals("android/telephony/TelephonyManager") && name.equals("getDeviceId") && desc.equals("()Ljava/lang/String;")) {
            Log.e("asmcode", "get imei className:%s, method:%s, name:%s", className, methodName, name);
        }
    }
}

After building, the following output will be displayed:

Overall, the difficulty of getting started with ASM is higher than that of AspectJ. We need to understand the bytecode after compilation. The ASM Bytecode Outline plugin recommended by class monitor Pengfei is a good helper! Finally, I uploaded the code I practiced to GitHub, where you can also find a Chinese version of the ASM documentation. Interested students can download and read it.

References:

  • “练习Sample跑起来 | ASM插桩强化练” (Practice Sample Running | Enhanced Exercise of ASM Instrumentation)
  • ASM documentation