网易NAPM Andorid SDK实现原理--转
原文地址:https://neyoufan.github.io/2017/03/10/android/NAPM%20Android%20SDK/
NAPM 是網易的應用性能管理平臺,采用非侵入的方式獲取應用性能數據,可以實時展示多個維度的分析結果。本文主要給大家分享一下Android端SDK的實現原理。
前言
APM(Application Performance Management),應用性能管理,主要是為了解決應用上線之后,性能問題難以發現、難以定位的問題,通過接入APM,可以實時了解應用在運行過程中的性能表現,快速定位和修復問題。
目前國內外有不少的應用性能管理平臺,例如國外的 New Relic、AppDynamics,國內的聽云、OneAPM,國內各大公司也都有自己的性能監控體系。
我們也開發了自己的平臺?NAPM?供公司內部的產品使用,移動端目前主要采集了網絡性能、交互性能和數據(數據庫、JSON、Image)處理性能數據,網絡性能目前主要采集了Http請求過程中的一些性能指標,比如響應時間、首包時間、DNS時間等,同時再結合機型、版本、地理位置、運營商、網絡環境等多個維度,就可以使用戶方便地了解應用在各種狀態下的性能表現,從而及時發現問題,做出適當的調整,達到優化用戶體驗的目的。
下圖是NAPM平臺某個應用的多維分析展示界面
Alt pic
接下來主要給大家分享一下網易NAPM Android端SDK的實現原理。
Android APM基本原理
簡單來說,一個APM平臺的工作流程大致如下:在各端(移動端、前端、后端)采集性能數據,然后上傳到后端進行建模、存儲,由平臺進行分析、挖掘,最后通過可視化的方式展示給用戶。
移動端SDK實際上只是一個數據采集系統,負責收集并上傳終端上產生的性能數據,大致可以劃分為三個模塊,最底層是數據采集模塊,負責采集各種性能數據,采集到的數據經過簡單的處理之后存儲在內存或者數據庫中,最上層是數據的消費模塊,通常會將采集到的數據上傳到后臺,供平臺存儲、分析和展示,同時我們也支持將采集到的性能數據交給用戶處理,方便用戶挖掘有用信息。
Alt pic
這里我們使用到了數據庫,主要是因為存在一些情況,會導致采集到的數據不能實時發送至后臺
- 當網絡狀態較差,上傳失敗
- 當前無可用網絡連接,無法上傳
- 當前網絡狀態不滿足上傳條件(用戶可以設置,比如僅在wifi的狀態下上傳數據)
因此我們需要將數據進行存儲,在合適的時機上傳到后臺,盡量保證數據的完整。
APM SDK的難點是數據的采集,手動埋點的方式無疑是行不通的,一方面代價太大且容易產生錯誤,另一方面對于沒有源代碼的第三方庫我們無法直接修改,因而不能滿足我們的需求。參考New Relic,我們選擇在應用構建期間通過修改字節碼的方式來進行代碼插樁。
首先我們看一下應用構建的過程:
Alt pic
可以看到,應用中所有的class文件包括引用的第三方庫中的class,都會經由dex過程,被轉化為一個或者多個dex文件,正因為所有的class文件都會在dex這一步被處理,所以我們選擇在這里進行字節碼插樁。
javaagent + Instrumentation
dex的過程是在dx程序中進行,而dx程序是由java實現的,這里我們使用到了javaagent技術,它可以使我們在JVM加載class文件前對字節碼作出修改,這里簡單介紹一下用法,主要分為兩步
實現javaagent
javaagent的形式是一個jar包,根據javaagent的不同加載方式,對它的實現也有不同的要求。
如果javaagent是在虛擬機啟動之后加載的,我們需要在它的manifest文件中指定Agent-Class屬性,它的值是javaagent的實現類,這個實現類需要實現一個agentmain方法
public static void agentmain(String agentArgs, Instrumentation instrumentation) {//xxx }agentmain會成為javaagent的入口,它會在javaagent被加載時調用。
但是如果javaagent是在JVM啟動時通過命令行參數加載的,情況會不太一樣,需要在它的manifest文件中指定Premain-Class屬性,它的值是javaagent的實現類,這個實現類需要實現一個premain方法。
public static void premain(String agentArgs, Instrumentation instrumentation) {//xxx }我們知道,一個java程序的入口是main方法,而如果javaagent是在JVM啟動時通過命令行參數加載的,虛擬機會在應用的main方法執行之前調用javaagent的premain方法,這應該也是premain方法名字的由來吧。
如果要支持兩種加載方式,那么上述的條件需要同時滿足。并且如果通過命令行參數在JVM啟動時加載,agentmain方法不會再被調用。而在這個時候,應用中的類還沒有被加載到虛擬機,所以給我們修改字節碼帶來了便利,因為一個類被加載之后,修改它的字節碼會比較麻煩。
我們看到premain方法的第二個參數是一個Instrumentation的實例,Instrumentation接口有一個方法
void addTransformer(ClassFileTransformer transformer, boolean canRetransform)它會在虛擬機中注冊一個ClassFileTransformer,transformer會在類加載時對類進行處理,ClassFileTransformer接口只定義了一個方法
byte[] transform(ClassLoader loader,String className,Class<?> classBeingRedefined,ProtectionDomain protectionDomain,byte[] classfileBuffer)throws IllegalClassFormatException而這個方法的作用就是修改一個類的字節碼,className是這個類的名稱,classfileBuffer是這個類原本的字節碼,而返回值是修改過后的字節碼,如果沒有修改,可以直接返回null。
因此,如果我們想在程序運行前改變一個類的字節碼,可以在javaagent的premain方法中調用Instrumentation的實例的addTransformer方法,添加一個自定義的ClassFileTransformer。偽代碼如下:
//實現一個javaagent,注冊自定義的ClassFileTransformer public class MyJavaAgent { public static void premain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException { inst.addTransformer(new MyTransformer()); } }//實現一個 ClassFileTransformer,對xxx.xxx.xxx類的字節碼進行修改 public class MyTransformer implements ClassFileTransformer {public byte[] transform(ClassLoader classLoader, String className, Class<?> clazz,ProtectionDomain protectionDomain, byte[] bytes) throws IllegalClassFormatException {if(name.equals("xxx.xxx.xxx")) {return changeByteCode(bytes);}return null;} }加載javaagent
前邊已經提到了javaagent有兩種加載方式
1) JVM啟動時通過命令行參數加載javaagent
- manifest中需要指定Premain-Class屬性
- 需要實現premain方法
- premain方法會在程序的main方法之前執行
-
agentmain方式不會被調用
通過命令行加載javaagent的形式如下:
-javaagent:jarpath[=options]一個示例如下:
java -javaagent:/path/to/myagent.jar -jar myapp.jar
2) JVM啟動后動態加載javaagent
- manifest中需要指定Agent-Class屬性
- 需要實現agentmain方法
-
agentmain方法會在javaagent被加載時執行
一般運行時加載agent的方法如下:
String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName(); int p = nameOfRunningVM.indexOf('@'); String pid = nameOfRunningVM.substring(0, p);String jarFilePath = "/the/path/to/the/agent/jar";try {VirtualMachine vm = VirtualMachine.attach(pid);vm.loadAgent(jarFilePath);vm.detach(); } catch (Exception e) {throw new RuntimeException(e); }
具體使用細節可參考VirtualMachine介紹http://docs.oracle.com/javase/7/docs/jdk/api/attach/spec/com/sun/tools/attach/VirtualMachine.html
借助javaagent,我們可以將代碼插樁的工作分為兩個步驟:首先是獲取到應用中所有的字節碼,然后是對應用的字節碼進行修改。
獲取應用字節碼
首先從要解決的問題出發,上邊提到我們會在dex的這一步去獲取字節碼,通過查看dx程序的代碼,我們發現,在dex的過程中所有的class文件會經由com.android.dx.command.dexer.Main的processClass()方法進行處理,processClass()的代碼如下:
/*** Processes one classfile.** @param name {@code non-null;} name of the file, clipped such that it* <i>should</i> correspond to the name of the class it contains* @param bytes {@code non-null;} contents of the file* @return whether processing was successful*/ private boolean processClass(String name, byte[] bytes) {if (! args.coreLibrary) {checkClassName(name);}try {new DirectClassFileConsumer(name, bytes, null).call(new ClassParserTask(name, bytes).call());} catch(Exception ex) {throw new RuntimeException("Exception parsing classes", ex);}return true; }第一個參數是應用中一個類的名字,第二個參數就是這個類的字節碼了,應用中所有的類,都會經過這個函數進行處理。
所以我們打算修改com.android.dx.command.dexer.Main的processClass()方法,從而獲取到應用中的字節碼,那么現在的問題就變成了如何修改com.android.dx.command.dexer.Main的processClass()方法。
掌握了javaagent,想要修改dx程序中com.android.dx.command.dexer.Main的字節碼就變得比較容易了,我們需要實現一個javaagent,在其中注冊一個ClassFileTransformer,在ClassFileTransformer的transform()方法中對com.android.dx.command.dexer.Main的字節碼進行修改,最后在dx程序啟動時將這個javaagent加載進去就好了。
//實現一個 ClassFileTransformer,對com.android.dx.command.dexer.Main類的字節碼進行修改 public class MainTransformer implements ClassFileTransformer {public byte[] transform(ClassLoader classLoader, String className, Class<?> clazz,ProtectionDomain protectionDomain, byte[] bytes) throws IllegalClassFormatException {if(name.equals("com/android/dx/command/dexer/Main")) {return changeMainClassByteCode(bytes);}return null;} }byte[] changeMainByteCode(byte[] bytes) {//修改Main的 processClass() 方法//返回修改后Main的字節碼 }如果你是通過命令行來手動構建應用的,到這里已經可以用上邊的方式獲取到應用中的字節碼了,然而大多數人在開發Android的時候,并不會通過命令行去手動構建,而是通過使用一些構建工具,來完成自動化構建,而dx程序則是由構建工具啟動的,所以我們面臨的問題就是如何將javaagent加載到dx進程。
我們目前支持了ant構建和gradle構建,通過查看ant和gradle的代碼,我們發現最終它們都會通過java.lang.ProcessBuilder的start()方法來啟動dx進程。
通過查看java.lang.ProcessBuilder的代碼,我們發現它有一個成員
private List<String> command;它是用來保存的是啟動目標進程的命令和參數,我們需要做的就是在調用start()方法啟動dx進程時,將加載javaagent的參數(-javaagent:jarpath[=options])添加到command中。
這里我們仍然使用javaagent來完成這個工作,我們需要實現另外一個javaagent,在其中注冊一個另一個ClassFileTransformer,在它的transform方法中對java.lang.ProcessBuilder的字節碼進行修改。
//實現一個 ClassFileTransformer,對com.android.dx.command.dexer.Main類的字節碼進行修改 public class ProcessBuilderTransformer implements ClassFileTransformer {public byte[] transform(ClassLoader classLoader, String className, Class<?> clazz,ProtectionDomain protectionDomain, byte[] bytes) throws IllegalClassFormatException {if(name.equals("java/lang/ProcessBuilder")) {return changeProcessBuilderClassByteCode(bytes);}return null;} }byte[] changeProcessBuilderClassByteCode(byte[] bytes) {//修改ProcessBuilder的 start() 方法//返回修改后ProcessBuilder的字節碼 }那么最終問題就變成了如何把這個javaagent加載到ant進程和gradle進程。
它們對應到了javaagent的兩種加載方式
-
ant構建-JVM啟動時加載
export ANT_OPTS="-javaagent:/path/to/agent.jar"(mac os環境,windows不太一樣)在ant構建前進行上述配置,可以在啟動ant時加載指定的javaagent,這里使用的是在JVM啟動時通過命令行參數加載javaagent的方式。
-
gradle構建 -JVM啟動后加載
我們會編寫一個gradle插件來完成javaagent的加載,當我們的插件被加載時,gradle進程已經運行起來了,因此只能通過動態的方式加載javaagent。
因此,獲取字節碼的流程,大致如下圖所示:
Alt pic
這個過程中主要使用了兩個javaagent,一個用來修改ProcessBuilder類,另一個用來修改Main類,涉及到的進程是ant構建進程或者gradle構建進程,以及由它們啟動的dx進程。
對于gradle構建方式,需要注意一點,gradle plugin 在2.1.0之后的版本,支持dx in-process,它使得dx的過程可以直接在當前的gradle進程中執行,而不需要額外啟動一個dx進程,從而縮短應用構建的時間。如果你在使用Android Studio構建應用的時候看到To run dex in process, the Gradle daemon needs a larger heap. It currently has 910 MB這樣的一句話,它就是指導用戶通過配置gradle daemon進程的堆大小來開啟dx in-process特性的。
而這個新的特性,會給我們設置javaagent帶來麻煩,不啟動dx進程使得我們無法對dx進程設置javaagent,而在gradle進程中動態加載javaagent時,com.android.dx.command.dexer.Main類早已經加載過了,所以通過javaagent方式來獲取字節碼會變得十分困難。
幸運的是,gradle plugin 在1.5.0之后,提供了一個Transform API,它允許第三方插件操作編譯后的class文件,而修改的時機正是在將這些字節碼轉換為dex文件之前,這里就不在展開講解了,感興趣的同學可以參考下這篇文章http://blog.csdn.net/sbsujjbcy/article/details/50839263。
修改應用字節碼
通過javaagent修改com.android.dx.command.dexer.Main和java.lang.ProcessBuilder,以及最終修改應用的字節碼進行插樁,都需要對.class文件的格式以及java虛擬機有比較深入的了解,另外需要使用字節碼操作工具來幫助我們對字節碼進行改造,這里不詳細講解,只是推薦一些有用的的字節碼操作框架和工具,后邊可能會有同事做相關的分享。
-
ASM是一個 Java 字節碼操控框架。它能被用來動態生成類或者增強既有類的功能。ASM 可以直接產生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態改變類行為。
-
Javassist是一個開源的分析、編輯和創建Java字節碼的類庫,它提供了源碼級別的API以及字節碼級別的API,源碼級別的API,直接使用java編碼的形式,而不需要深入了解虛擬機指令,就能動態改變類的結構或者動態生成類。
-
Bytecode Outline plugin for Eclipse是一個非常有用的eclipse 插件,可以查看當前正在編輯的java文件或者class文件的字節碼。
-
如果需要逆向APK,查看字節碼修改的效果,除了dex2jar外,再給大家推薦一個google的逆向工具enjarify。
小結
本文重點介紹了使用javaagent在應用打包過程中修改com.android.dx.command.dexer.Main和java.lang.ProcessBuilder的字節碼,從而獲取到應用的字節碼,進行插樁的基本原理,并沒有涉及so hook相關的原理,以后有機會的話會再做一次分享。
轉載于:https://www.cnblogs.com/davidwang456/p/7550789.html
總結
以上是生活随笔為你收集整理的网易NAPM Andorid SDK实现原理--转的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JVM源码分析之javaagent原理完
- 下一篇: pyCrypto python 3.5-