preface

To be a good Android developer, you need a complete set ofThe knowledge systemHere, let us grow together into what we think ~.

Nowadays, compilation and piling technology has been deep into various fields of Android development, and AOP technology is a kind of efficient implementation of piling mode, its emergence just brings light to us in the dark, and greatly solves some pain points in the traditional development process. AspectJ, as an extended set of section-oriented design specifications based on the Java language, can give us new capabilities. In this article we’ll learn how to use AspectJ for staking. The contents of this article are as follows:

  • 1) Classification and application scenarios of compilation and piling technology.
  • 2) Advantages and limitations of AspectJ.
  • 3) Introduction to AspectJ core syntax.
  • 4) AspectJX combat
  • 5) Build your own performance monitoring framework using AspectJX.
  • 6) Summary.

Aspect-oriented programming (AOP) has attracted the attention of many developers, but how to effectively implement this set of design concepts in coding is not easy. Fortunately, as early as 2003, A set of section-oriented extension designs based on the Java language: AspectJ was born.

Unlike traditional OOP programming, AspectJ (or AOP) is unique in finding problems that can’t be handled very well using traditional programming methods. For example, a problem to enforce security policies in some applications. Security is an issue throughout the system between all modules, and each module has to add security to guarantee the security of the whole application and security module itself also need security, obviously the security strategy of the implementation of the problem is a cross-cutting concerns, using traditional programming to solve this problem is very difficult and prone to errors, This is where AOP comes in.

Traditional object-oriented programming, each unit is a class, which is similar to the safety problems in this respect, they are often unable to concentrate processing in a class, because they are across multiple classes, which leads to code cannot be reused, and they are not reliable and not to inherit, such programming makes poor maintainability and produced a large number of redundant code, This is not what we want to see.

The advent of section-oriented programming brings light to the darkness. It treats these crosscutting concerns just like object-oriented programming treats general concerns.

Before we dive further into AOP programming, it is worth looking at the current taxonomy and application scenarios for compile piling techniques. This allows us to understand the relationship and function of each technology point from a higher dimension.

I. Classification and application scenarios of compilation and piling technology

Compilation piling technology can be divided into two categories, as follows:

  • 1),APT (Annotation Process Tools)Used to generate Java code.
  • 2),AOP (Aspect Oriented Programming):Used to manipulate bytecode.

Below 👇, we will introduce their functions in detail respectively.

1. APT (Annotation Process Tools)

Common annotation generation frameworks such as ButterKnife, Dagger, GreenDao, and Protocol Buffers are known to generate code during compilation. The timing of generating code using Android Annotations combined with APT technology comes at the beginning of compilation. But AOP adds or modifies code logic directly by modifying the.class file before the dex file is generated after compilation.

The way to generate Java code using APT technology has the following two advantages:

  • 1) Isolated the complex internal implementation of the framework, making development more simple and efficient.
  • 2) it greatly reduces the workload of manual repetition and reduces the probability of making mistakes during development.

AOP (Aspect Oriented Programming)

As for the way of manipulating bytecode, it is generally used in the three scenarios of code monitoring, code modification and code analysis.

In contrast to Java code generation, the way bytecode is manipulated has the following characteristics:

  • 1) Wider application scenarios.
  • 2) More powerful functions.
  • 3) High complexity.

In addition, we can manipulate Java bytecode in.class files as well as Dalvik bytecode in.dex files. Here we have a general understanding of how the compilation and piling technology is applied in the above three scenarios.

1. Code monitoring

In addition to power consumption monitoring, compilation and piling technology can realize various performance monitoring, such as network data monitoring, time-consuming method monitoring, big picture monitoring, thread monitoring and so on.

For example, the realization of network data monitoring is to realize the whole process monitoring of network requests through hook network library method and automatic injection of interceptor at the network layer, including obtaining information of various network stages such as handshake duration, first packet time, DNS time, network time and so on.

After realizing the monitoring of network request process, we can analyze the data performance of the whole network process in detail, find the problem points of network level performance, and make targeted optimization measures. For example, to solve the problem of high network error rate, we can take the following measures, as follows:

  • 1) Use HttpDNS.
  • 2) Synchronize error logs to THE CDN.
  • 3) Optimization of CDN scheduling link.

2. Code modification

There are a lot of scenarios in which code modification is realized by compilation and staking technology, and the most frequently used scenarios can be divided into the following four types:

  • 1),Achieve no trace burying pointSuch as: * *Netease HubbleData’s Android no-buried practice, [51 Credit card Android automatic buried point practice

] (mp.weixin.qq.com/s/P95ATtgT2…

  • 2),Handle click jitter in a unified manner:Compilation phase unified hook android. View. The view. The OnClickListener# onClick () method, to achieve a rapid click invalid shake effect, this can be highly effective, non invasive unified solution to the client quickly click repeatedly lead to frequency response problems.
  • 3),Third-party SDK Dr Processing:We can temporarily modify or hook the third-party SDK before going online to achieve quick online disaster recovery.
  • 4),Implement the hot repair framework:We can build Gradle automatically, that is, after Java source code is compiled and before dex file is generated. The purpose of staking is to find out whether there is a corresponding patch method according to the signature of each method when executing each method. If there is, execute the patch method. If not, execute your own logic.

3. Code analysis

Third party code inspection tools such as Findbugs also use compile-pegging for custom code inspection, which can be used to find inappropriate Hanlder usage, new Thread calls, sensitive permission calls, and more.

Advantages and limitations of AspectJ

The most common bytecode processing frameworks are AspectJ, ASM, and so on, all of which have the same thing in that their inputs and outputs are Class files. Also, they are executed after Java files are compiled into.class files and before Dalvik bytecode is generated.

As a popular AOP (Aspect-Oriented Programming) programming extension framework in Java, AspectJ internally uses BCEL framework to complete its functions. Let’s take a look at some of AspectJ’s advantages.

1. Advantages of AspectJ

It has two advantages: it is mature, stable and very simple to use.

1. Mature and stable

The processing of bytecode is not easy, especially for the format of bytecode and various instruction rules. If the processing error, it will lead to problems in the process of compiling or running the program. AspectJ, which has been around since 2001, is so mature that it usually doesn’t have to worry about the correctness of inserted bytecode generation.

2. Very simple to use

AspectJ is simple to use and powerful enough to manipulate bytecodes in many situations without any knowledge of Java bytecodes. For example, it can insert custom code in five places:

  • 1) Where methods (including constructors) are called.
  • 2) Inside the method body (including the constructor).
  • 3) In the position of reading and writing variables.
  • 4) Inside a static code block.
  • 5) Before and after the position of exception handling.

In addition, it can directly replace the code in the original location with custom code.

2. AspectJ’s flaws

AspectJ’s shortcomings can be summed up as follows:

1. Fixed entry point

AspectJ can only operate at fixed pointcuts, but it is difficult to do more nuanced operations on bytecode sequences with specific rules.

2. Limitations of regular expressions

AspectJ uses regular express-like matching rules, such as the onXXX method that matches the Activity lifecycle. If any custom method starts with on, it will also match, so that the matching correctness cannot be satisfied.

3, low performance

AspectJ wraps its own specific classes when implemented. Instead of inserting Trace functions directly into code, it goes through a series of wraps of its own. This not only generates large bytecode, but also has a significant impact on the performance of the original function. If you want to pile all the functions in the App, the performance impact will be quite large. If you plug only a small number of functions, the performance cost of AspectJ is negligible.

AspectJ core syntax introduction

AspectJ is actually a kind of AOP framework, AOP is a technology to achieve unified maintenance of program functions. AOP can be used to isolate each part of the business logic, so as to reduce the coupling between the parts of the business logic, improve the reusability of the program, and greatly improve the development efficiency. So AOP’s advantages can be summarized as follows:

  • 1) Non-invasive.
  • 2), easy to modify.

Furthermore, unlike OOP, which divides problems into single modules, AOP addresses the same class of problems that involve many modules in a unified manner. For example, we can design two aspects, one is to handle the log output function of all modules in the App, and the other is to handle the permission check of some special function calls in the App.

At 👇, we’ll take a look at some of the core concepts we need to understand to master AspectJ.

1. Crosscutting concerns

Which methods to intercept and what to do with them.

2. Aspect

A class is an abstraction of object features, and a section is an abstraction of crosscutting concerns.

3, JoinPoint (JoinPoint)

JPoint is the key execution point of a program, and it’s where we focus our attention. It is the point (method, field, constructor, and so on) that is intercepted.

4. PointCut

Definition of interception to JoinPoint. The purpose of PointCut is to provide a way for developers to select JoinPoint that they are interested in.

5. Advice

Pointcuts are only used to capture collections of join points, but they do nothing but capture collections of join points. In fact, to implement crosscutting behavior, we’re going to use notifications. It generally refers to the code to be executed after intercepting JoinPoint, divided into front, back, surround three types. In this case, we need to pay attention to the Advice Precedence. For example, if we use both @before and @around for the same aspect method, an error will be reported, and the Advice Precedence will be set.

AspectJ is a set of section-oriented programming specifications implemented based on Java language. It adds the new concept of Join Point to Java, which is really just the name of an existing Java concept. It adds a few new constructs to the Java language, such as pointcuts, Advice, Inter-type declarations, and aspects. Pointcuts and advice dynamically affect program flow, intertype declarations statically affect the class-level structure of the program, and sections encapsulate all of these new structures.

For each of the core concepts in AsepctJ, the join points are appropriate points in the program flow. Pointcuts collect a specific set of join points and the values within those points. A notification is the code that executes when a join point arrives, which is a dynamic part of AspectJ. A pointcut is a breakpoint set at a particular statement. It collects information about the program stack at the breakpoint, and the notification is the program code to be added before and after the breakpoint.

In addition, There are many different types of inter-type declarations in AspectJ, which allow programmers to modify the static structure of a program, names, class members, and relationships between classes. Aspects in AspectJ are modular units of crosscutting concerns. They behave much like classes in the Java language, but aspects also encapsulate pointcuts, advice, and inter-type declarations.

Using AspectJ on Android is a bit of a hassle, so we can use The AspectJX framework of Hujiang directly. Now, let’s use AspectJX for AOP aspect programming.

AspectJX in action

First, in order to use AOP on Android, AspectJX needs to be introduced. Add it to the project root directory under build.gradle:

    classpath 'com. Hujiang. Aspectjx: gradle - android plugin - aspectjx: 2.0.0'
Copy the code

Then, under build.gradle in your app directory, add:

    apply plugin: 'android-aspectjx'
    implement 'org. Aspectj: aspectjrt: 1.8 +'
Copy the code

JoinPoint is generally located in the following positions:

  • 1) Function calls.
  • 2) Get and set variables.
  • 3) Class initialization.

Use PointCut to intercept the join points we specify, and Advice to intercept the code to execute after JoinPoint. Advice generally comes in three types:

  • 1), Before: execute Before PointCut
  • 2), After: execute After PointCut
  • 3), Around: Before and after PointCut.

1. Simplest AspectJ example

First, let’s take a small chestnut 🌰 :

    @Before("execution(* android.app.Activity.on**(..) )"
    public void onActivityCalled(JoinPoint joinPoint) throws Throwable {
        Log.d(...)
    }
Copy the code

Execution is a matching rule where the first * matches any method return value, followed by syntactic code that matches all Activity methods starting with on. This allows us to print a log in all activities in our App that start with on.

Execution of the above execution is the type of Join Point processing, usually of one of two types:

  • 1), call: represents the position of the call method, inserted outside the function body.
  • Execution: insert into the body of a function.

2. Count the time consuming of all methods in Application

So, how do we use it to count all method times in the Application?

    @Aspect
    public class ApplicationAop {
    
        @Around("call (* com.json.chao.application.BaseApplication.**(..) )"
        public void getTime(ProceedingJoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.toShortString();
        long time = System.currentTimeMillis();
        try {
            joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        Log.i(TAG, name + " cost"+ (System.currentTimeMillis() - time)); }}Copy the code

Note that when Action is Before or After, the method entry parameter is JoinPoint. When Action is Around, the method entry parameter is ProceedingPoint.

The main difference between Around and Before and After is that ProceedingPoint differs from JoinPoint in that it provides the PROCEED method to execute the target method.

3. Peg all methods in App with Systrace function

In an in-depth look at Android Startup Speed Optimization, I talked about using Systrace to peg functions so that you can see the time and CPU usage of methods in your application. With AspectJ, we can use it to peg all methods in our App with Systrace, as shown below:

    @Aspect
    public class SystraceTraceAspectj {

        private static final String TAG = "SystraceTraceAspectj";

        @Before("execution(* **(..) )"
        public void before(JoinPoint joinPoint) {
            TraceCompat.beginSection(joinPoint.getSignature().toString());
        }
    
        @After("execution(* **(..) )"
        public void after(a) { TraceCompat.endSection(); }}Copy the code

Now that you know the basics of AspectJX, let’s use it and AspectJ to build a simplified version of APM (Performance Monitoring Framework).

Build your own performance monitoring framework using AspectJ

Now, we will use the ArgusAPM performance monitoring framework of qihoo 360 to comprehensively analyze the application of AOP technology in performance monitoring. It is mainly divided into the following three parts:

  • 1) Monitor the startup time and life cycle time of the application.
  • 2) Monitor every network request of OKHttp3.
  • 3) Monitor every HttpConnection network request.

1. Monitor the startup time and life cycle time of the application

ArgusAPM implements TraceActivity, an Activity slice file that monitors the application’s hot and cold startup time and lifecycle time. The code for TraceActivity is shown below:

    @Aspect
    public class TraceActivity {

        // define a pointcut method baseCondition that excludes the corresponding classes in Argusapm.
        @Pointcut("! within(com.argusapm.android.aop.*) && ! within(com.argusapm.android.core.job.activity.*)")
        public void baseCondition(a) {}// 2. Define a pointcut applicationOnCreate that executes Application's onCreate method.
        @Pointcut("execution(* android.app.Application.onCreate(android.content.Context)) && args(context)")
        public void applicationOnCreate(Context context) {}/ / 3, define a rear inform applicationOnCreateAdvice, used in the application of the onCreate method execution after insert AH. ApplicationOnCreate (context) this line of code.
        @After("applicationOnCreate(context)")
        public void applicationOnCreateAdvice(Context context) {
            AH.applicationOnCreate(context);
        }

        // 4. Define a pointcut to execute the attachBaseContext method of Application.
        @Pointcut("execution(* android.app.Application.attachBaseContext(android.content.Context)) && args(context)")
        public void applicationAttachBaseContext(Context context) {}/ / 5, and define a pre notice, is used to insert before the application method of onAttachBaseContext AH. ApplicationAttachBaseContext (context) this line of code.
        @Before("applicationAttachBaseContext(context)")
        public void applicationAttachBaseContextAdvice(Context context) {
            AH.applicationAttachBaseContext(context);
        }

        // define a pointcut that executes all Activity methods that start with on, followed by "&& baseCondition()" to exclude classes in ArgusAPM.
        @Pointcut("execution(* android.app.Activity.on**(..) ) && baseCondition()")
        public void activityOnXXX(a) {}Define a wrap notification to insert code at the start and end of all activities' on methods. (Excludes classes in ArgusAPM)
        @Around("activityOnXXX()")
        public Object activityOnXXXAdvice(ProceedingJoinPoint proceedingJoinPoint) {
            Object result = null;
            try {
                Activity activity = (Activity) proceedingJoinPoint.getTarget();
                // Log.d("AJAOP", "Aop Info" + activity.getClass().getCanonicalName() +
                // "\r\nkind : " + thisJoinPoint.getKind() +
                // "\r\nargs : " + thisJoinPoint.getArgs() +
                // "\r\nClass : " + thisJoinPoint.getClass() +
                // "\r\nsign : " + thisJoinPoint.getSignature() +
                // "\r\nsource : " + thisJoinPoint.getSourceLocation() +
                // "\r\nthis : " + thisJoinPoint.getThis()
                / /);
                long startTime = System.currentTimeMillis();
                result = proceedingJoinPoint.proceed();
                String activityName = activity.getClass().getCanonicalName();

                Signature signature = proceedingJoinPoint.getSignature();
                String sign = "";
                String methodName = "";
                if(signature ! =null) {
                    sign = signature.toString();
                    methodName = signature.getName();
                }

                if(! TextUtils.isEmpty(activityName) && ! TextUtils.isEmpty(sign) && sign.contains(activityName)) { invoke(activity, startTime, methodName, sign); }}catch (Exception e) {
                e.printStackTrace();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
            return result;
        }

        public void invoke(Activity activity, long startTime, String methodName, String sign) { AH.invoke(activity, startTime, methodName, sign); }}Copy the code

We have noticed that in the comments 4, 5, the two code is used to insert before the application method of onAttachBaseContext AH. ApplicationAttachBaseContext (context) this line of code. In addition, the code in comments 2 and 3 is used to insert the ah. applicationOnCreate(context) line after the application onCreate method has been executed. Now, let’s look at the implementation of these two methods in the AH class. The code is as follows:

    public static void applicationAttachBaseContext(Context context) {
        ActivityCore.appAttachTime = System.currentTimeMillis();
        if (Env.DEBUG) {
            LogX.d(Env.TAG, SUB_TAG, "applicationAttachBaseContext time : "+ ActivityCore.appAttachTime); }}public static void applicationOnCreate(Context context) {
        if (Env.DEBUG) {
            LogX.d(Env.TAG, SUB_TAG, "applicationOnCreate"); }}Copy the code

As you can see, in the AH class will start time appAttachTime applicationAttachBaseContext method records the ActivityCore instances. And applicationOnCreate basically implements nothing.

We then go back to the aspect file TraceActivity and see the code in comments 6 and 7, which are used to insert the corresponding code at the beginning and end of all activities’ on methods. Note that the classes in ArgusAPM are excluded.

Let’s examine the actions in the activityOnXXXAdvice method. First, the startTime is obtained before the target method is executed. Then, call the proceedingJoinPoint. Proceed () is used to perform the target method; Finally, the invoke method of the AH class is called. Let’s look at the processing of the invoke method, which looks like this:

    public static void invoke(Activity activity, long startTime, String lifeCycle, Object... extars) {
        / / 1
        boolean isRunning = isActivityTaskRunning();
        if (Env.DEBUG) {
            LogX.d(Env.TAG, SUB_TAG, lifeCycle + " isRunning : " + isRunning);
        }
        if(! isRunning) {return;
        }

        / / 2
        if (TextUtils.equals(lifeCycle, ActivityInfo.TYPE_STR_ONCREATE)) {
            ActivityCore.onCreateInfo(activity, startTime);
        } else {
            / / 3
            int lc = ActivityInfo.ofLifeCycleString(lifeCycle);
            if (lc <= ActivityInfo.TYPE_UNKNOWN || lc > ActivityInfo.TYPE_DESTROY) {
                return; } ActivityCore.saveActivityInfo(activity, ActivityInfo.HOT_START, System.currentTimeMillis() - startTime, lc); }}Copy the code

First, in comment 1, we check to see if the Activity count task for the current application is enabled. If it does, it will then go to comment 2, where it will determine if the target method name is “onCreate”. If it is, ActivityCore’s onCreateInfo method will be executed as follows:

    // Check whether it is the first startup
    public static boolean isFirst = true;
    public static long appAttachTime = 0;
    // Boot type
    public static int startType;
    
    public static void onCreateInfo(Activity activity, long startTime) {
        / / 1
        startType = isFirst ? ActivityInfo.COLD_START : ActivityInfo.HOT_START;
        / / 2
        activity.getWindow().getDecorView().post(new FirstFrameRunnable(activity, startType, startTime));
        / / onCreate time
        long curTime = System.currentTimeMillis();
        / / 3
        saveActivityInfo(activity, startType, curTime - startTime, ActivityInfo.TYPE_CREATE);
    }
Copy the code

First, in comment 1, the startup type is recorded at this point, and the first startup defaults to a cold start. Then, in comment 2, a Runnable is posted when the first frame is displayed. Finally, at comment 3, saveActivityInfo is called to save information about the target method. Let’s first look at the code for the FirstFrameRunnable run method, as follows:

     @Override
        public void run(a) {
            if (DEBUG) {
                LogX.d(TAG, SUB_TAG, "FirstFrameRunnable time:" + (System.currentTimeMillis() - startTime));
            }
            / / 1
            if ((System.currentTimeMillis() - startTime) >= ArgusApmConfigManager.getInstance().getArgusApmConfigData().funcControl.activityFirstMinTime) {
                saveActivityInfo(activity, startType, System.currentTimeMillis() - startTime, ActivityInfo.TYPE_FIRST_FRAME);
            }
            if (DEBUG) {
                LogX.d(TAG, SUB_TAG, "FirstFrameRunnable time:" + String.format("[%s, %s]", ActivityCore.isFirst, ActivityCore.appAttachTime));
            }
            if (ActivityCore.isFirst) {
                ActivityCore.isFirst = false;
                if (ActivityCore.appAttachTime <= 0) {
                    return;
                }
                / / 2
                int t = (int) (System.currentTimeMillis() - ActivityCore.appAttachTime);
                AppStartInfo info = new AppStartInfo(t);
                ITask task = Manager.getInstance().getTaskManager().getTask(ApmTask.TASK_APP_START);
                if(task ! =null) {
                    / / 3
                    task.save(info);
                    if (AnalyzeManager.getInstance().isDebugMode()) {
                        / / 4AnalyzeManager.getInstance().getParseTask(ApmTask.TASK_APP_START).parse(info); }}else {
                    if (DEBUG) {
                        LogX.d(TAG, SUB_TAG, "AppStartInfo task == null");
                    }
                }
            }
        }
    }
Copy the code

First, in comment 1, it calculates the current first frame time, the cold start time of the Activity, and compares it to activityFirstMinTime (the default value is 300ms). If the Activity’s cold start time is greater than 300ms, the cold start time is saved by calling saveActivityInfo.

Then, in comment 2, we record the startup time of the App and save it to the task instance AppStartTask in comment 3. Finally, in note 4, if it is in debug mode, AnalyzeManager will be called the data analysis management singleton class getParseTask method to obtain the AppStartParseTask instance, the key code is as follows:

    private Map<String, IParser> mParsers;
    
    private AnalyzeManager(a) {
        mParsers = new HashMap<String, IParser>(3);
        mParsers.put(ApmTask.TASK_ACTIVITY, new ActivityParseTask());
        mParsers.put(ApmTask.TASK_NET, new NetParseTask());
        mParsers.put(ApmTask.TASK_FPS, new FpsParseTask());
        mParsers.put(ApmTask.TASK_APP_START, new AppStartParseTask());
        mParsers.put(ApmTask.TASK_MEM, new MemoryParseTask());
        this.isUiProcess = Manager.getContext().getPackageName().equals(ProcessUtils.getCurrentProcessName());
    }

    public IParser getParseTask(String name) {
        if (TextUtils.isEmpty(name)) {
            return null;
        }
        return mParsers.get(name);
    }
Copy the code

The Parse method of the AppStartParseTask class is then called, which you can see is a class designed to analyze application startup time in Debug mode. The code for the parse method looks like this:

    /** * app starts **@param info
     */
    @Override
    public boolean parse(IInfo info) {
        if (info instanceof AppStartInfo) {
            AppStartInfo aInfo = (AppStartInfo) info;
            if (aInfo == null) {
                return false;
            }
            try {
                JSONObject obj = aInfo.toJson();
                obj.put("taskName", ApmTask.TASK_APP_START);
                / / 1
                OutputProxy.output("Startup time :" + aInfo.getStartTime(), obj.toString());
            } catch (JSONException e) {
                e.printStackTrace();
            }
            DebugFloatWindowUtls.sendBroadcast(aInfo);
        }
        return true;
    }
Copy the code

In comment 1, the parse method simply continues by calling OutputProxy’s Output method to pass in the startup time and the string that records the startup information. Let’s look at the output method of OutputProxy as follows:

    /** * Alarm message output **@param showMsg
     */
    public static void output(String showMsg) {
        if(! AnalyzeManager.getInstance().isDebugMode()) {return;
        }
        if (TextUtils.isEmpty(showMsg)) {
            return;
        }
        // 1. Store it locally
        StorageManager.saveToFile(showMsg);
    }
Copy the code

Note 1, in the output method, the StorageManager saveToFile method continues to call the start information stored locally, saveToFile implementation code is shown as follows:

    /** * save to text file ** by line@param line
     */
    public static void saveToFile(String line) {
        TraceWriter.log(Env.TAG, line);
    }
Copy the code

Here, the log method of TraceWriter is called to save the startup information to the text file line by line, and the key code is as follows:

    public static void log(String tagName, String content) {
        log(tagName, content, true);
    }

    private synchronized static void log(String tagName, String content, boolean forceFlush) {
        if (Env.DEBUG) {
            LogX.d(Env.TAG, SUB_TAG, "tagName = " + tagName + " content = " + content);
        }
        if (sWriteThread == null) {
            / / 1
            sWriteThread = new WriteFileRun();
            Thread t = new Thread(sWriteThread);
            t.setName("ApmTrace.Thread");
            t.setDaemon(true);
            t.setPriority(Thread.MIN_PRIORITY);
            t.start();

            String initContent = "---- Phone=" + Build.BRAND + "/" + Build.MODEL + "/verName:" + "--";
            / / 2
            sQueuePool.offer(new Object[]{tagName, initContent, Boolean.valueOf(forceFlush)});
            if (Env.DEBUG) {
                LogX.d(Env.TAG, SUB_TAG, "init offer content = "+ content); }}if (Env.DEBUG) {
            LogX.d(Env.TAG, SUB_TAG, "offer content = " + content);
        }
        / / 3
        sQueuePool.offer(new Object[]{tagName, content, Boolean.valueOf(forceFlush)});

        synchronized(LOCKER_WRITE_THREAD) { LOCKER_WRITE_THREAD.notify(); }}Copy the code

In comment 1, if the sWriteThread Runnable for writing log information does not exist, the low-priority daemon thread for writing log information is created and started.

SQueuePool’s Offer method is then called at comment 2 to save the relevant information, which is of type ConcurrentLinkedQueue, indicating that it is a queue dedicated to a concurrent environment. If Runnable already exists, log information is enqueued directly in comment 3. Finally, sQueuePool’s poll() method is called in the run method of sWriteThread to pull out the log information and store it locally through the BufferWriter-wrapped FileWriter.

At this point, we are done analyzing the handling of the onCreate method, and then we return to comment 3 of the Invoke method to analyze cases where the onCreate method is not present. If the method name is not onCreate, ActivityInfo’s ofLifeCycleString method is called. Let’s look at its implementation as follows:

    /** * The lifecycle string is converted to a numeric value **@param lcStr
     * @return* /
    public static int ofLifeCycleString(String lcStr) {
        int lc = 0;
        if (TextUtils.equals(lcStr, TYPE_STR_FIRSTFRAME)) {
            lc = TYPE_FIRST_FRAME;
        } else if (TextUtils.equals(lcStr, TYPE_STR_ONCREATE)) {
            lc = TYPE_CREATE;
        } else if (TextUtils.equals(lcStr, TYPE_STR_ONSTART)) {
            lc = TYPE_START;
        } else if (TextUtils.equals(lcStr, TYPE_STR_ONRESUME)) {
            lc = TYPE_RESUME;
        } else if (TextUtils.equals(lcStr, TYPE_STR_ONPAUSE)) {
            lc = TYPE_PAUSE;
        } else if (TextUtils.equals(lcStr, TYPE_STR_ONSTOP)) {
            lc = TYPE_STOP;
        } else if (TextUtils.equals(lcStr, TYPE_STR_ONDESTROY)) {
            lc = TYPE_DESTROY;
        }
        return lc;
    }
Copy the code

As you can see, ofLifeCycleString converts life cycle strings into numeric values. Here’s how they are defined:

    /** * Activity lifecycle type enumeration */
    public static final int TYPE_UNKNOWN = 0;
    public static final int TYPE_FIRST_FRAME = 1;
    public static final int TYPE_CREATE = 2;
    public static final int TYPE_START = 3;
    public static final int TYPE_RESUME = 4;
    public static final int TYPE_PAUSE = 5;
    public static final int TYPE_STOP = 6;
    public static final int TYPE_DESTROY = 7;
    
    /** * The name of the Activity lifecycle type value */
    public static final String TYPE_STR_FIRSTFRAME = "firstFrame";
    public static final String TYPE_STR_ONCREATE = "onCreate";
    public static final String TYPE_STR_ONSTART = "onStart";
    public static final String TYPE_STR_ONRESUME = "onResume";
    public static final String TYPE_STR_ONPAUSE = "onPause";
    public static final String TYPE_STR_ONSTOP = "onStop";
    public static final String TYPE_STR_ONDESTROY = "onDestroy";
    public static final String TYPE_STR_UNKNOWN = "unKnown";
Copy the code

We then go back to comment 3 of the Invoke method of the AH class and call ActivityCore’s saveActivityInfo method only if the name of the method is the one defined above, that is, the Acitivity lifecycle method or the first frame method. The implementation code for this method is as follows:

    public static void saveActivityInfo(Activity activity, int startType, long time, int lifeCycle) {
        if (activity == null) {
            if (DEBUG) {
                LogX.d(TAG, SUB_TAG, "saveActivityInfo activity == null");
            }
            return;
        }
        if (time < ArgusApmConfigManager.getInstance().getArgusApmConfigData().funcControl.activityLifecycleMinTime) {
            return;
        }
        String pluginName = ExtraInfoHelper.getPluginName(activity);
        String activityName = activity.getClass().getCanonicalName();
        activityInfo.resetData();
        activityInfo.activityName = activityName;
        activityInfo.startType = startType;
        activityInfo.time = time;
        activityInfo.lifeCycle = lifeCycle;
        activityInfo.pluginName = pluginName;
        activityInfo.pluginVer = ExtraInfoHelper.getPluginVersion(pluginName);
        if (DEBUG) {
            LogX.d(TAG, SUB_TAG, "apmins saveActivityInfo activity:" + activity.getClass().getCanonicalName() + " | lifecycle : " + activityInfo.getLifeCycleString() + " | time : " + time);
        }
        ITask task = Manager.getInstance().getTaskManager().getTask(ApmTask.TASK_ACTIVITY);
        boolean result = false;
        if(task ! =null) {
            result = task.save(activityInfo);
        } else {
            if (DEBUG) {
                LogX.d(TAG, SUB_TAG, "saveActivityInfo task == null"); }}if (DEBUG) {
            LogX.d(TAG, SUB_TAG, "activity info:" + activityInfo.toString());
        }
        if (AnalyzeManager.getInstance().isDebugMode()) {
            AnalyzeManager.getInstance().getActivityTask().parse(activityInfo);
        }
        if (Env.DEBUG) {
            LogX.d(TAG, SUB_TAG, "saveActivityInfo result:"+ result); }}Copy the code

As you can see, the logic here is simple, just save the log information in the ActivityInfo instance and save the ActivityInfo instance in the ActivityTask. Note that The ActivityTask instance is already stored in the taskMap HashMap object when argusapm.init () is called. The key code is as follows:

    /** * Register task: Each new task is registered, i.e. the corresponding xxxTask instance is placed into the taskMap collection. * /
    public void registerTask(a) {
        if (Env.DEBUG) {
            LogX.d(Env.TAG, "TaskManager"."registerTask " + getClass().getClassLoader());
        }
        if (Build.VERSION.SDK_INT >= 16) {
            taskMap.put(ApmTask.TASK_FPS, new FpsTask());
        }
        taskMap.put(ApmTask.TASK_MEM, new MemoryTask());
        taskMap.put(ApmTask.TASK_ACTIVITY, new ActivityTask());
        taskMap.put(ApmTask.TASK_NET, new NetTask());
        taskMap.put(ApmTask.TASK_APP_START, new AppStartTask());
        taskMap.put(ApmTask.TASK_ANR, new AnrLoopTask(Manager.getContext()));
        taskMap.put(ApmTask.TASK_FILE_INFO, new FileInfoTask());
        taskMap.put(ApmTask.TASK_PROCESS_INFO, new ProcessInfoTask());
        taskMap.put(ApmTask.TASK_BLOCK, new BlockTask());
        taskMap.put(ApmTask.TASK_WATCHDOG, new WatchDogTask());
    }
Copy the code

Next, let’s look at the implementation of the ActivityTask class, as follows:

    public class ActivityTask extends BaseTask {

        @Override
        protected IStorage getStorage(a) {
            return new ActivityStorage();
        }

        @Override
        public String getTaskName(a) {
            return ApmTask.TASK_ACTIVITY;
        }

        @Override
        public void start(a) {
            super.start();
            if(Manager.getInstance().getConfig().isEnabled(ApmTask.FLAG_COLLECT_ACTIVITY_INSTRUMENTATION) && ! InstrumentationHooker.isHookSucceed()) {/ / hook failure
                if (DEBUG) {
                    LogX.d(TAG, "ActivityTask"."CanWork hook: Hook failed");
                }
                mIsCanWork = false; }}@Override
        public boolean isCanWork(a) {
            returnmIsCanWork; }}Copy the code

BaseTask (); BaseTask (); BaseTask (); BaseTask (); BaseTask ();

    /** * ArgusAPM task base class **@author ArgusAPM Team
    */
    public abstract class BaseTask implements ITask {...@Override
        public boolean save(IInfo info) {
            if (DEBUG) {
                LogX.d(TAG, SUB_TAG, "save task :" + getTaskName());
            }
            / / 1
            returninfo ! =null&& mStorage ! =null&& mStorage.save(info); }... }Copy the code

In comment 1, we continue with a call to the mStorage save method, which is an interface IStorage. Obviously, the implementation class here is the ActivityStorage instance returned in ActivityTask’s getStorage() method. It is an Activity storage class that handles Activity information. At this point, monitoring application hot and cold startup time and life cycle time part of the analysis is completed.

Let’s take a look at how AspectJ can be used to monitor every network request to OKHttp3.

Monitor every network request to OKHttp3

First, we see the section file for OKHttp3, which looks like this:

    /** * OKHTTP3 slice file **@author ArgusAPM Team
    */
    @Aspect
    public class OkHttp3Aspect {

        // define a pointcut that calls the build method of OkHttpClient directly.
        @Pointcut("call(public okhttp3.OkHttpClient build())")
        public void build(a) {}Use surround notification to add a NetWokrInterceptor before the build method executes.
        @Around("build()")
        public Object aroundBuild(ProceedingJoinPoint joinPoint) throws Throwable {
            Object target = joinPoint.getTarget();

            if (target instanceof OkHttpClient.Builder && Client.isTaskRunning(ApmTask.TASK_NET)) {
                OkHttpClient.Builder builder = (OkHttpClient.Builder) target;
                builder.addInterceptor(new NetWorkInterceptor());
            }

            returnjoinPoint.proceed(); }}Copy the code

In comments 1 and 2, a NetWokrInterceptor is added before calling the build method of OkHttpClient. Let’s look at the implementation code, as follows:

    @Override
    public Response intercept(Chain chain) throws IOException {
        // get the start time of each OkHttp request
        long startNs = System.currentTimeMillis();

        mOkHttpData = new OkHttpData();
        mOkHttpData.startTime = startNs;

        if (Env.DEBUG) {
            Log.d(TAG, "Okhttp request start time:" + mOkHttpData.startTime);
        }

        Request request = chain.request();
        
        // 2. Record the request URL and request data size of the current request
        recordRequest(request);

        Response response;

        try {
            response = chain.proceed(request);
        } catch (IOException e) {
            if (Env.DEBUG) {
                e.printStackTrace();
                Log.e(TAG, "HTTP FAILED: " + e);
            }
            throw e;
        }
        
        // 3, record the request time
        mOkHttpData.costTime = System.currentTimeMillis() - startNs;

        if (Env.DEBUG) {
            Log.d(TAG, "Okhttp chain.proceed Time:" + mOkHttpData.costTime);
        }
        
        // 4. Record the response code and size of the response data returned by the current request
        recordResponse(response);

        if (Env.DEBUG) {
            Log.d(TAG, "okhttp chain.proceed end.");
        }

        // 5. Record OkHttp request data
        DataRecordUtils.recordUrlRequest(mOkHttpData);
        return response;
    }
Copy the code

First, in comment 1, you get the start time of each OkHttp request. Then, in note 2, the request URL and request data size for the current request are recorded through the recordRequest method. Then, at comment 3, the time taken for this request is recorded.

Next, in note 4, the response code returned by the current request and the size of the response data are recorded by the recordResponse method. Finally, in annotation 5, the recordUrlRequest method that calls DataRecordUtils records the data saved in mOkHttpData. We continue with the recordUrlRequest method, which looks like this:

    /**
     * recordUrlRequest
     *
     * @param okHttpData
     */
    public static void recordUrlRequest(OkHttpData okHttpData) {
        if (okHttpData == null || TextUtils.isEmpty(okHttpData.url)) {
            return;
        }

        QOKHttp.recordUrlRequest(okHttpData.url, okHttpData.code, okHttpData.requestSize,
                okHttpData.responseSize, okHttpData.startTime, okHttpData.costTime);

        if (Env.DEBUG) {
            Log.d(Env.TAG, "Store okkHttp request data, end."); }}Copy the code

As you can see, the recordUrlRequest method of QOKHttp is called to record network request information. Let’s look at the recordUrlRequest method for QOKHttp as follows:

    /** * Records a network request **@paramUrl Request URL *@paramCode Status code *@paramRequestSize Specifies the data size to be sent *@paramResponseSize Size of data received *@paramStartTime startTime *@paramCostTime time-consuming * /
    public static void recordUrlRequest(String url, int code, long requestSize, long responseSize,
                                        long startTime, long costTime) {
        NetInfo netInfo = new NetInfo();
        netInfo.setStartTime(startTime);
        netInfo.setURL(url);
        netInfo.setStatusCode(code);
        netInfo.setSendBytes(requestSize);
        netInfo.setRecordTime(System.currentTimeMillis());
        netInfo.setReceivedBytes(responseSize);
        netInfo.setCostTime(costTime);
        netInfo.end();
    }
Copy the code

As you can see, the network request information is saved in NetInfo and the end method of NetInfo is finally called as follows:

    /** * why is the stored operation written here? * Historical reasons */
    public void end(a) {
        if (DEBUG) {
            LogX.d(TAG, SUB_TAG, "end :");
        }
        this.isWifi = SystemUtils.isWifiConnected();
        this.costTime = System.currentTimeMillis() - startTime;
        if (AnalyzeManager.getInstance().isDebugMode()) {
            AnalyzeManager.getInstance().getNetTask().parse(this);
        }
        ITask task = Manager.getInstance().getTaskManager().getTask(ApmTask.TASK_NET);
        if(task ! =null) {
            / / 1
            task.save(this);
        } else {
            if (DEBUG) {
                LogX.d(TAG, SUB_TAG, "task == null"); }}}Copy the code

As you can see, the NetTask instance’s save method is finally called to save the network request information. NetTask must use the corresponding NetStorage instance to store the information in the ContentProvider. This concludes the analysis of the OkHttp3 section.

For an application that uses OkHttp3, the above implementation is effective in retrieving network request information, but what if the application does not use OkHttp3? At this point, we can only monitor each HttpConnection network request. Here’s how to do it.

Monitor every network request to HttpConnection and HttPClient

In ArgusAPM, the aspect class TraceNetTrafficMonitor is used to monitor every HttpConnection network request. The key codes are as follows:

    @Aspect
    public class TraceNetTrafficMonitor {

        / / 1
        @Pointcut("(! within(com.argusapm.android.aop.*) && ((! within(com.argusapm.android.**) && (! within(com.argusapm.android.core.job.net.i.*) && (! within(com.argusapm.android.core.job.net.impl.*) && (! within(com.qihoo360.mobilesafe.mms.transaction.MmsHttpClient) && ! target(com.qihoo360.mobilesafe.mms.transaction.MmsHttpClient)))))))")
        public void baseCondition(a) {}/ / 2
        @Pointcut("call(org.apache.http.HttpResponse org.apache.http.client.HttpClient.execute(org.apache.http.client.methods.HttpUriRequest)) && (target(httpClient) && (args(request) && baseCondition()))")
        public void httpClientExecuteOne(HttpClient httpClient, HttpUriRequest request) {}/ / 3
        @Around("httpClientExecuteOne(httpClient, request)")
        public HttpResponse httpClientExecuteOneAdvice(HttpClient httpClient, HttpUriRequest request) throws IOException {
            return QHC.execute(httpClient, request);
        }

        // Check some aspect code that handles exceptions

        / / 4
        @Pointcut("call(java.net.URLConnection openConnection()) && (target(url) && baseCondition())")
        public void URLOpenConnectionOne(URL url) {}/ / 5
        @Around("URLOpenConnectionOne(url)")
        public URLConnection URLOpenConnectionOneAdvice(URL url) throws IOException {
            return QURL.openConnection(url);
        }

        // Check some aspect code that handles exceptions
    
    }
Copy the code

The operations in TraceNetTrafficMonitor fall into two categories: the execute method used to cut HttpClient, as shown in comments 1, 2, and 3. One is the openConnection method used to cut HttpConnection. The corresponding section code is in comments 4 and 5. QHC: execute (); QHC: execute ();

    public static HttpResponse execute(HttpClient client, HttpUriRequest request) throws IOException {
        return isTaskRunning()
                ? AopHttpClient.execute(client, request)
                : client.execute(request);
    }
Copy the code

The execute method of AopHttpClient is called as follows:

    public static HttpResponse execute(HttpClient httpClient, HttpUriRequest request) throws IOException {
        NetInfo data = new NetInfo();
        / / 1
        HttpResponse response = httpClient.execute(handleRequest(request, data));
        / / 2
        handleResponse(response, data);
        return response;
    }
Copy the code

First, at comment 1, handleRequest is called to process the request data, as follows:

    private static HttpUriRequest handleRequest(HttpUriRequest request, NetInfo data) {
        data.setURL(request.getURI().toString());
        if (request instanceof HttpEntityEnclosingRequest) {
            HttpEntityEnclosingRequest entityRequest = (HttpEntityEnclosingRequest) request;
            if(entityRequest.getEntity() ! =null) {
                // wrap the request entity with AopHttpRequestEntity
                entityRequest.setEntity(new AopHttpRequestEntity(entityRequest.getEntity(), data));
            }
            return (HttpUriRequest) entityRequest;
        }
        return request;
    }
Copy the code

As you can see in comment 1, the request entity is encapsulated with AopHttpRequestEntity, primarily to facilitate data manipulation using NetInfo in the encapsulated entity. Next, in comment 2, the resulting response information is processed. The implementation here is simple: use the NetInfo entity class to store the response information in the ContentProvider. At this point, the HttpClient processing part of our analysis is complete.

Next, let’s analyze the HTTPConnection section code, as shown below:

    / / 4
    @Pointcut("call(java.net.URLConnection openConnection()) && (target(url) && baseCondition())")
    public void URLOpenConnectionOne(URL url) {}/ / 5
    @Around("URLOpenConnectionOne(url)")
    public URLConnection URLOpenConnectionOneAdvice(URL url) throws IOException {
        return QURL.openConnection(url);
    }
Copy the code

As you can see, the openConnection method of QURL is called for processing. Let’s take a look at its implementation code:

    public static URLConnection openConnection(URL url) throws IOException {
        return isNetTaskRunning() ? AopURL.openConnection(url) : url.openConnection();
    }
Copy the code

Here we call the openConnection method of AopURL again.

    public static URLConnection openConnection(URL url) throws IOException {
        if (url == null) {
            return null;
        }
        return getAopConnection(url.openConnection());
    }
    
    private static URLConnection getAopConnection(URLConnection con) {
        if (con == null) {
            return null;
        }
        if (Env.DEBUG) {
            LogX.d(TAG, "AopURL"."getAopConnection in AopURL");
        }
        
        / / 1
        if ((con instanceof HttpsURLConnection)) {
            return new AopHttpsURLConnection((HttpsURLConnection) con);
        }
        
        / / 2
        if ((con instanceof HttpURLConnection)) {
            return new AopHttpURLConnection((HttpURLConnection) con);
        }
        return con;
    }
Copy the code

Finally, in comment 1, it is determined that con will be encapsulated using AopHttpsURLConnection if it is an HTTPS request and AopHttpURLConnection if it is an HTTP request. The AopHttpsURLConnection implementation is similar, with the addition of SSL certificate authentication. So here we will directly analyze the implementation of AopHttpURLConnection, here the code is very much, will not be posted, but its core processing can be summarized as the following two points:

  • 1), in the callback getHeaderFields (), getInputStream (), getLastModified () will call inspectAndInstrumentResponse method a series of methods, such as the size and status code stored in the response NetInfo.
  • 2) When calling onInputstreamComplete(), onInputstreamError() and other methods, i.e., when the request completes or fails, myData’s end method will be called directly to save the network response information in the ContentProvider.

At this point, the AOP implementation portion of ArgusAPM is fully analyzed.

Six, summarized

Finally, let’s review what we learned from this article, as follows:

  • 1.Classification and application scenarios of compiler piling technology.
    • 1) APT.
    • 2) AOP.
  • 2. Advantages and limitations of AspectJ.
  • 3. Introduction to AspectJ core syntax.
  • 4,AspectJX of actual combat.
    • 1) Simplest AspectJ example.
    • 2) Count the time consuming of all methods in Application.
    • 3) Carry out Systrace function piling for all methods in App.
  • 5,Use AspectJ to build your own performance monitoring framework.
    • 1) Monitor the startup time and life cycle time of the application.
    • 2) Monitor every network request of OKHttp3.
    • 3) Monitor every network request of HttpConnection and HttpClient.

As you can see, AOP is indeed powerful and there is a lot you can do with AspectJ, but it also has a number of drawbacks, such as fixed pointcuts, inflexibness due to inherent flaws in regular expressions, and the amount of wrapping code it generates. So, is there a better way to implement it that is more flexible in use and avoids generating wrapping code to reduce the performance cost of staking? Yes, it is ASM, but it needs to manipulate JVM bytecodes to do code staking, which is quite difficult to get started with, so in the next article we will learn more about JVM bytecodes

Reference links:


There are three ways to build a peg: AspectJ, ASM, and ReDex

2. AspectJ Programming Guide PDF

3, The AspectJ 5 Development Kit Developer’s Notebook

4. In-depth understanding of Android AOP

5. Application and practice of AOP technology in client

6, Pay attention to the Advice Precedence

7, AspectJX

8. BCEL framework

9. Use of AspectJ in ArgusAPM, 360’s performance monitoring framework

10, the use of AspectJ to achieve piling examples

Thank you for reading this article and I hope you can share it with your friends or technical group, it means a lot to me.

I hope we can be friends inGithub,The Denver nuggetsTo share knowledge.