1. 程式人生 > >Android APM方案(一)完成程式碼注入

Android APM方案(一)完成程式碼注入

什麼是APM

APM 是Application perfmance monitor的簡稱, 應用效能監控。在移動網際網路對人們生活影響越來越大的今天,App的功能越來越全面,從吃穿住行,到支付開房,全方面覆蓋。相同功能的App存在很多競品,比如攜程和藝龍,天貓和京東,網易雲音樂和QQ音樂。隨之而來的就是App效能的要求越來越高,不能被動的等待使用者異常的發生然後根據線上日誌去修復bug,再發補丁版本。主動監控App效能,變得越來越重要,分析App的耗電,UI卡頓,網路效能(Socket連線時間,首位元組接受時間等等)成為了當物之急。但是如何能在不更改業務方程式碼的同時完成一個移動端的監控呢?AOP成為了我們一個很好的選擇,我們首先了解一些基本概念。

一些基本概念

  • JavaAgent

代理 (agent) 是在你的main方法前的一個攔截器 (interceptor),也就是在main方法執行之前,執行agent的程式碼。

主要作用
可以在載入class檔案之前做攔截,對位元組碼做修改
agent的程式碼與你的main方法在同一個JVM中執行,並被同一個system classloader裝載,被同一的安全策略 (security policy) 和上下文 (context) 所管理。

用法

public class MyAgent {
    public static void agentmain(String args, Instrumentation instrumentation){
        permain(args, instrumentation);
    }

    public
static void permain(String args, Instrumentation instrumentation){ instrumentation.addTransformer(new MainTransformer()); } }

如果javaagent是在虛擬機器啟動之後載入的,我們需要在它的manifest檔案中指定Agent-Class屬性,它的值是javaagent的實現類,這個實現類需要實現一個agentmain方法

public static void agentmain(String args, Instrumentation instrumentation){
        permain(args, instrumentation);
    }

但是如果javaagent是在JVM啟動時通過命令列引數載入的,情況會不太一樣,需要在它的manifest檔案中指定Premain-Class屬性,它的值是javaagent的實現類,這個實現類需要實現一個premain方法。

public static void permain(String args, Instrumentation instrumentation){
        instrumentation.addTransformer(new MainTransformer());
    }
  • Instrumentation

從Agent的兩個方法可以看到都會傳入2個引數,一個是引數agrs,另一個就是Instrumentation。那Instrumentation是什麼呢

來看一段官方的解釋

java.lang.instrument
public interface Instrumentation
This class provides services needed to instrument Java programming language code. Instrumentation is the addition of byte-codes to methods for the purpose of gathering data to be utilized by tools. Since the changes are purely additive, these tools do not modify application state or behavior. Examples of such benign tools include monitoring agents, profilers, coverage analyzers, and event loggers.
There are two ways to obtain an instance of the Instrumentation interface:

  1. When a JVM is launched in a way that indicates an agent class. In
    that case an Instrumentation instance is passed to the premain
    method of the agent class.
  2. When a JVM provides a mechanism to start agents sometime after the
    JVM is launched. In that case an Instrumentation instance is passed
    to the agentmain method of the agent code.

大致意思是該類提供了用於設計Java程式語言程式碼所需的服務。同時舉了兩個改介面被例項化的例子。在permain和agentMain這個2個方法,jvm會提供2個被例項化的介面供我們呼叫。我們在這裡只用到了addTransformer這個方法

 /**
     * Registers the supplied transformer.
     * <P>
     * Same as <code>addTransformer(transformer, false)</code>.
     *
     * @param transformer          the transformer to register
     * @throws java.lang.NullPointerException if passed a <code>null</code> transformer
     * @see    #addTransformer(ClassFileTransformer,boolean)
     */
    void
    addTransformer(ClassFileTransformer transformer);

它向JVM提供一個我們實現的ClassFileTransformer

  • ClassFileTransformer

再來看下ClassFilesTransformer是什麼

同樣是一段官方API註釋

java.lang.instrument

public interface ClassFileTransformer

An agent provides an implementation of this interface in order to transform class files. The transformation occurs before the class is defined by the JVM.
Note the term class file is used as defined in section 3.1 of The Java™ Virtual Machine Specification, to mean a sequence of bytes in class file format, whether or not they reside in a file.

Agent提供了一個實現該介面的例項,用來轉換我們的Class檔案。這種轉換髮生在JVM加在這些class檔案之前。

那到底是如何轉換的呢
讓我們看看transform()這個方法

byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;

JVM向我們提供了該class的類載入器,類名,型別,還有最關鍵的位元組碼,然後我們利用下節會講到的ASM工具,把classfileBuffer位元組碼改造成我們想要的樣子,然後返回給JVM,就能達到我們靜態織入位元組碼的目的的。我們可以利用這套流程幹很多監控的事,比如統計網路情況,現在很流行的插樁埋點,統計方法時長。


如何實現hook

一個完整的無侵入APM分為3部分

  1. Agent
  2. plugin
  3. 業務程式碼

Agent很好理解,hook的入口,幫助我們獲得每個類的位元組碼

plugin把Agent掛載到JVM上,在main()入口前,讓Agent發揮它的作用

業務程式碼,根據我們的實際需求,改造獲得的位元組碼,從而達到修改目的碼的目的

首先看下Agent的實現

  • 新建一個Agent類
public class MyAgent {
    public static void agentmain(String args, Instrumentation instrumentation){
        permain(args, instrumentation);
    }

    public static void permain(String args, Instrumentation instrumentation){
        instrumentation.addTransformer(new MainTransformer());
    }

}
  • 然後實現我們的ClassFileTransformer
public class MainTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader classLoader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)
            throws IllegalClassFormatException {
        System.out.println("className : " + className);
        return classfileBuffer;
    }


}

這樣一個最簡單的Agent就實現了,等後面實現具體業務的時候,把

System.out.println("className : " + className);

替換成我們的實際業務改造程式碼就行了

最後主要在MANIFEST.MF檔案中加上

Premain-Class: com.apm.MyAgent  //對應premain方法
Agent-Class: com.apm.MyAgent    //對應agentmain方法

打成jar包備用

  • 構建一個plugin

plugin的作用是把我們之前編寫的Agent外掛注入到JVM裡去。

由於我使用的IDE是Android Studio,所以我選擇的Plugin的承載形式是Gradle外掛。

然後選擇編寫這個Gradle外掛的IDE是IntelliJ IDEA,大家可以自行下載一下。

下載完IDE之後,我們需要新建一個Gralde外掛的Project

這裡寫圖片描述

這裡千萬千萬注意,Project SDK 這裡要選擇1.7的JDK,如果選擇1.8的話後面會引起Plugin加入到AndroidStudio後 ,報一個

If you are using the ‘java’ gradle plugin in a library submodule add targetCompatibility = ‘1.7’ 的錯誤

新建完專案之後,需要在root根目錄下新建一個libs的資料夾,加入我們之前編寫的apm.jar和Java\jdk1.7.0_17\lib下面的tools.jar,但是要注意打包的時候千萬不要把這2個jar包打進去。

新建一個Java Class作為plugin

public class ApmPlugin implements Plugin<Project>{
    @Override
    public void apply(Project project) {
         //得到虛擬機器的名字
        String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName();
        int p = nameOfRunningVM.indexOf('@');
        String pid = nameOfRunningVM.substring(0, p);
        try {
            //這裡jar包的路徑,就取一個Agent裡的檔案的路徑就好了
            //這也是為什麼我們要引入Agent.jar
            String jarFilePath = MyAgent.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
            jarFilePath = new File(jarFilePath).getCanonicalPath();
            VirtualMachine vm = VirtualMachine.attach(pid);
            vm.loadAgent(jarFilePath);
            vm.detach();
        } catch (URISyntaxException | IOException | AgentInitializationException | AttachNotSupportedException | AgentLoadException e) {
            throw new RuntimeException(e);
        }
    }
}

有了plugin之後,我們要告訴編譯器這個plugin在哪

於是,在目錄下新建resources目錄,然後在resources目錄裡面再新建META-INF目錄,再在META-INF裡面新建gradle-plugins目錄。最後在gradle-plugins目錄裡面新建properties檔案。這個properties檔案的名字可以是任取的,但是後面加在Plugin的時候,會用到這個名字。比如你新建了一個myagent.properties,那麼你AndroidStudio的app module就需要加上

apply plugin: 'myagent'

然後在myagent.properties檔案裡面指明你自定義的類

implementation-class=com.apm.myagent.ApmPlugin

再然後就是最後一步了,打包成一個jar,供AndroidStudio使用

開啟Project Structure(快捷鍵 Ctrl + Alt + Shift + S)

再Project Settings中找到Artifacts,然後按照下圖配置

這裡寫圖片描述

最後在Build中找到Build Artifacts點選之後再點選Build就能生成最後的Plugin的Jar包了

  • 最後的配置

到目前為止我們已經有了2個jar包,一個Agent.jar,另一個Plugin.jar

然後再我們AndroidStudio的專案根目錄下新建一個plugin目錄

這裡寫圖片描述

然後再專案的gradle檔案中加入(注意不是module的gradle檔案)

dependencies {
        classpath 'com.android.tools.build:gradle:2.3.1'
        classpath fileTree(dir: 'plugin', include: '*.jar')
    }

最後在App module的gradle檔案中加入

apply plugin: 'openapm'

最後在Build中選擇MakeProject,然後Clean

我們可以在控制檯中看見

這裡寫圖片描述

在這裡被坑了下,這裡的資料不會再Android Monitor輸出,是在Gradle Console中顯示

至此,我們已經成功把我們的Agent掛載到JVM上了,至於要編寫哪些功能

再下一章節我們可以會修改Agent,完成一些真實的監控場景

參考文獻