文章目錄
前言
關於AOP思想和AspectJX框架大家都耳熟能詳,AspectJ爲開發者提供了實現AOP的基礎能力,可以通過它來實現符合各自業務需求的功能。
這裏藉助AspectJX框架來實現效能提升相關的一些有意思的功能,AspectJX框架的配置和使用在README中有詳細步驟,也可以參考官方demo。
AspectJ中的語法說明詳見:
https://github.com/hiphonezhu/Android-Demos/blob/master/AspectJDemo/AspectJ.pdf
https://github.com/HujiangTechnology/AspectJX-Demo/blob/master/docs/aspectj_manual.md
場景實戰
日誌打印
日常開發中,經常會在某個關鍵方法中打印Log輸出一段字符串和參數變量的值來進行分析調試,或者在方法執行前後打印Log來查看方法執行的耗時。
痛點
如果需要在業務主流程中的多個關鍵方法中增加日誌,查看方法執行的輸入參數和返回結果是否正確,只能繁瑣的在每個方法開頭添加Log調用打印輸出每個參數。若該方法有返回值,則在return前再添加Log打印輸出返回值。若該方法中有多個if分支進行return,還得在每個分支return前打印Log。
統計方法耗時需要在方法開頭記錄時間,在每個return前計算時間並打印Log。不僅繁瑣,還容易遺漏。
解決
可以通過給想要打印日誌的方法上標記一個註解,在編譯時給標記註解的方法織入代碼,自動打印這個方法運行時的輸入輸出信息和耗時信息。
定義註解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AutoLog {
/** logcat篩選tag */
String tag();
/** 打印日誌級別(默認VERBOSE) */
LogLevel level() default LogLevel.VERBOSE;
}
自定義一個註解AutoLog,用於給想要打印日誌的方法做標記。
定義切面和切入點
@Aspect
public class LogAspect {
/**
* 切入點,添加了AutoLog註解的所有方法體內
*/
@Pointcut("execution (@com.cdh.aop.toys.annotation.AutoLog * *(..))")
public void logMethodExecute() {
}
// Advice ···
}
創建一個日誌切面LogAspect,在其中定義一個切入點,對所有添加了AutoLog註解的方法進行代碼織入。
切入點中的execution表示在該方法體內進行代碼織入,@com.cdh.aop.toys.annotation.AutoLog表示添加了該註解的方法,第一個星表示不限return type,第二個星表示匹配任意方法名稱,(…)表示不限方法入參。
@Aspect
public class LogAspect {
// Pointcut ···
/**
* 對上面定義的切入點的方法進行織入,Around的作用是替代原方法體內代碼
*/
@Around("logMethodExecute()")
public Object autoLog(ProceedingJoinPoint joinPoint) {
try {
// 獲取被織入方法的簽名信息,MethodSignature包含方法的詳細信息
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// 獲取方法上添加的AutoLog註解
AutoLog log = methodSignature.getMethod().getAnnotation(AutoLog.class);
if (log != null) {
// 用於拼接日誌詳細信息
StringBuilder sb = new StringBuilder();
// 拼接方法名稱
String methodName = methodSignature.getMethod().getName();
sb.append(methodName);
// 拼接每個參數的值
Object[] args = joinPoint.getArgs();
if (args != null && args.length > 0) {
sb.append("(");
for (int i=0; i<args.length; i++) {
sb.append(args[i]);
if (i != args.length-1) {
sb.append(",");
}
}
sb.append(")");
}
// 記錄開始執行時的時間
long beginTime = System.currentTimeMillis();
// 執行原方法代碼,並獲得返回值
Object result = joinPoint.proceed();
// 計算方法執行耗時
long costTime = System.currentTimeMillis() - beginTime;
if (methodSignature.getReturnType() != void.class) {
// 若該方法返回類型不是void,則拼接返回值
sb.append(" => ").append(result);
}
// 拼接耗時
sb.append(" | ").append("cost=").append(costTime);
// 拼接方法所在類名和行號
String className = methodSignature.getDeclaringType().getSimpleName();
int srcLine = joinPoint.getSourceLocation().getLine();
sb.append(" | [").append(className).append(":").append(srcLine).append("]");
// 打印日誌,使用AutoLog註解設置的tag和級別調用Log類的對應方法
LogUtils.log(log.level(), log.tag(), sb.toString());
return result;
}
} catch (Throwable t) {
t.printStackTrace();
}
return null;
}
}
使用Around可以替換原方法中的邏輯,也可以通過ProceedingJoinPoint.proceed繼續執行原方法邏輯。這裏在執行原方法邏輯之外,還進行了方法參數信息的拼接和耗時計算,最後打印日誌輸出。
到這裏完成了一個基本的日誌切面織入功能,接下來在想要自動打印日誌的方法上添加註解即可。
使用示例
隨意寫幾個方法調用,在這幾個方法上添加AutoLog註解。
public class AddOpWithLog extends BaseOp {
public AddOpWithLog(BaseOp next) {
super(next);
}
@Override
@AutoLog(tag=TAG, level=LogLevel.DEBUG)
protected int onOperate(int value) {
return value + new Random().nextInt(10);
}
}
public class SubOpWithLog extends BaseOp {
public SubOpWithLog(BaseOp next) {
super(next);
}
@Override
@AutoLog(tag=TAG, level=LogLevel.WARN)
protected int onOperate(int value) {
return value - new Random().nextInt(10);
}
}
public class MulOpWithLog extends BaseOp {
public MulOpWithLog(BaseOp next) {
super(next);
}
@Override
@AutoLog(tag=TAG, level=LogLevel.WARN)
protected int onOperate(int value) {
return value * new Random().nextInt(10);
}
}
public class DivOpWithLog extends BaseOp {
public DivOpWithLog(BaseOp next) {
super(next);
}
@Override
@AutoLog(tag=TAG, level=LogLevel.DEBUG)
protected int onOperate(int value) {
return value / (new Random().nextInt(10)+1);
}
}
@AutoLog(tag = BaseOp.TAG, level = LogLevel.DEBUG)
public void doWithLog(View view) {
BaseOp div = new DivOpWithLog(null);
BaseOp mul = new MulOpWithLog(div);
BaseOp sub = new SubOpWithLog(mul);
BaseOp add = new AddOpWithLog(sub);
int result = add.operate(100);
Toast.makeText(this, result+"", Toast.LENGTH_SHORT).show();
}
運行doWithLog方法,查看logcat輸出日誌:
效果如圖所示,打印方法名稱以及每個入參的值和直接結果返回值(若是void則不打印返回值),還有該方法的執行耗時(單位ms)。
線程切換
日常開發中經常會涉及線程切換操作,例如網絡請求、文件IO和其他耗時操作需要放在自線程中執行,UI操作需要切回主線程執行。
痛點
每次切換線程時需要創建Runnable,在它的run方法中執行業務邏輯,或者利用AsyncTask和Executor(切回主線程還需要利用Handler),需要在方法調用處或方法體內部增加這些代碼來切換線程運行。
如果能通過給方法加個標記,就能自動讓該方法在主或子線程執行,就可以讓方法調用過程變得清晰和極大的減少代碼量。
解決
同樣可以利用註解給方法標記,在編譯器織入線程調用的代碼,自動進行線程切換。
注意:這裏的實現方案較爲雞肋,僅提供一個思路和演示。
定義註解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AutoThread {
/**
* 指定方法運行在主/子線程
* 可選枚舉值: MAIN(期望運行在主線程) BACKGROUND(期望運行在子線程)
*/
ThreadScene scene();
/**
* 設置是否阻塞等待該方法執行完成才返回(默認true)
*/
boolean waitUntilDone() default true;
}
AutoThread.java
自定義註解AutoThread,用於標記想要自動切換線程運行的方法。
定義切面和切入點
@Aspect
public class ThreadAspect {
@Pointcut("execution (@com.cdh.aop.toys.annotation.AutoThread * *(..))")
public void threadSceneTransition() {
}
// Advice ···
}
ThreadAspect.java
這裏定義了一個切面ThreadAspect和切入點threadSceneTransition。
切入點中的execution表示在該方法體內進行代碼織入,@com.cdh.aop.toys.annotation.AutoThread表示添加了該註解的方法,第一個星表示不限return type,第二個星表示匹配任意方法名稱,(…)表示不限方法入參。
@Aspect
public class ThreadAspect {
// Pointcut ···
@Around("threadSceneTransition()")
public Object executeInThread(final ProceedingJoinPoint joinPoint) {
// result用於保存原方法執行結果
final Object[] result = {null};
try {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// 獲取我們添加的方法註解AutoThread
AutoThread thread = methodSignature.getMethod().getAnnotation(AutoThread.class);
if (thread != null) {
// 獲取註解中設置的ThreadScene值,
ThreadScene threadScene = thread.scene();
if (threadScene == ThreadScene.MAIN && !ThreadUtils.isMainThread()) {
// 若期望運行在主線程,但當前不在主線程
// 切換到主線程執行
ThreadUtils.runOnMainThread(new Runnable() {
@Override
public void run() {
try {
// 執行原方法,並保存結果
result[0] = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}, thread.waitUntilDone());
} else if (threadScene == ThreadScene.BACKGROUND && ThreadUtils.isMainThread()) {
// 若期望運行在子線程,但當前在主線程
// 切換到子線程執行
ThreadUtils.run(new Runnable() {
@Override
public void run() {
try {
// 執行原方法,並保存結果
result[0] = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}, thread.waitUntilDone());
} else {
// 直接在當前線程運行
result[0] = joinPoint.proceed();
}
}
} catch (Throwable t) {
t.printStackTrace();
}
// 返回原方法返回值
return result[0];
}
}
這裏使用Around替換原方法邏輯,在執行原方法之前,先進行線程判斷,然後切換到對應線程再執行原方法。
線程切換方法
上面看到,當需要切換主線程時,調用ThreadUtils.runOnMainThread來執行原方法,看看這個方法的內部實現:
/**
* 主線程執行
*
* @param runnable 待執行任務
* @param block 是否等待執行完成
*/
public static void runOnMainThread(Runnable runnable, boolean block) {
if (isMainThread()) {
runnable.run();
return;
}
// 利用CountDownLatch來阻塞當前線程
CountDownLatch latch = null;
if (block) {
latch = new CountDownLatch(1);
}
// 利用Pair保存Runnable和CountDownLatch
Pair<Runnable, CountDownLatch> pair = new Pair<>(runnable, latch);
// 將Pair參數發送到主線程處理
getMainHandler().obtainMessage(WHAT_RUN_ON_MAIN, pair).sendToTarget();
if (block) {
try {
// 等待CountDownLatch降爲0
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static class MainHandler extends Handler {
MainHandler() {
super(Looper.getMainLooper());
}
@Override
public void handleMessage(Message msg) {
if (msg.what == WHAT_RUN_ON_MAIN) {
// 取出Pair參數
Pair<Runnable, CountDownLatch> pair = (Pair<Runnable, CountDownLatch>) msg.obj;
try {
// 取出Runnable參數運行
pair.first.run();
} finally {
if (pair.second != null) {
// 使CountDownLatch降1,這裏會降爲0,喚醒前面的阻塞等待
pair.second.countDown();
}
}
}
}
}
ThreadUtils.java
切換到主線程的方式還是利用主線程Handler。若設置等待結果返回,則會創建CountDownLatch,阻塞當前調用線程,等待主線程中執行完任務後才返回。
接下來看看切換子線程執行的方法ThreadUtils.run:
/**
* 子線程執行
*
* @param runnable 待執行任務
* @param block 是否等待執行完成
*/
public static void run(final Runnable runnable, final boolean block) {
Future future = getExecutorService().submit(new Runnable() {
@Override
public void run() {
// 通過線程池運行在子線程
runnable.run();
}
});
if (block) {
try {
// 等待執行結果
future.get();
} catch (Exception e) {
e.printStackTrace();
}
}
}
切換到子線程就是通過線程池提交任務執行。
使用示例
同樣寫幾個方法,然後加上AutoThread註解
public class AddOpInThread extends BaseOp {
public AddOpInThread(BaseOp next) {
super(next);
}
@Override
@AutoThread(scene = ThreadScene.BACKGROUND)
protected int onOperate(int value) {
// 打印該方法運行時所在線程
Log.w(BaseOp.TAG, "AddOpInThread onOperate: " + java.lang.Thread.currentThread());
return value + new Random().nextInt(10);
}
}
AddOpInThread.java
方法註解指定運行在子線程。
public class SubOpInThread extends BaseOp {
public SubOpInThread(BaseOp next) {
super(next);
}
@Override
@AutoThread(scene = ThreadScene.MAIN)
protected int onOperate(int value) {
// 打印該方法運行時所在線程
Log.w(BaseOp.TAG, "SubOpInThread onOperate: " + java.lang.Thread.currentThread());
return value - new Random().nextInt(10);
}
}
SubOpInThread.java
指定運行在主線程。
public class MulOpInThread extends BaseOp {
public MulOpInThread(BaseOp next) {
super(next);
}
@Override
@AutoThread(scene = ThreadScene.MAIN)
protected int onOperate(int value) {
// 打印該方法運行時所在線程
Log.w(BaseOp.TAG, "MulOpInThread onOperate: " + java.lang.Thread.currentThread());
return value * new Random().nextInt(10);
}
}
MulOpInThread.java
指定運行在主線程。
public class DivOpInThread extends BaseOp {
public DivOpInThread(BaseOp next) {
super(next);
}
@Override
@AutoThread(scene = ThreadScene.BACKGROUND)
protected int onOperate(int value) {
// 打印該方法運行時所在線程
Log.w(BaseOp.TAG, "DivOpInThread onOperate: " + java.lang.Thread.currentThread());
return value / (new Random().nextInt(10)+1);
}
}
DivOpInThread.java
指定運行在子線程。
接下來調用方法:
public void doWithThread(View view) {
BaseOp div = new DivOpInThread(null);
BaseOp mul = new MulOpInThread(div);
BaseOp sub = new SubOpInThread(mul);
BaseOp add = new AddOpInThread(sub);
int result = add.operate(100);
Toast.makeText(this, result+"", Toast.LENGTH_SHORT).show();
}
運行doWithThread方法,查看logcat輸出日誌:
可以看到第一個方法已經切換到子線程中運行,第二、三個方法又運行在主線程中,第四個方法又運行在子線程中。
線程名稱檢測
通常我們在創建使用Thread時,需要給它設置一個名稱,便於分析和定位該Thread所屬業務模塊。
痛點
開發過程中出現疏漏或者引入的第三方庫中不規範使用線程,例如直接創建線程運行,或者匿名線程等。當想要分析線程時,就會看到很多Thread-1、2、3的線程,如果有一個清晰的名稱就容易一眼看出該線程所屬的業務。
解決
可以通過攔截所有的Thread.start調用時機,在start之前檢測線程名稱。若是默認名稱,則進行警告,並且自動修改線程名稱。
定義切面和切入點
這裏把線程相關織入操作都放在一個切面ThreadAspect中:
@Aspect
public class ThreadAspect {
private static final String TAG = "ThreadAspect";
@Before("call (* java.lang.Thread.start(..))")
public void callThreadStart(JoinPoint joinPoint) {
try {
// 獲取joinPoint所在對象,即執行start方法的那個Thread實例
Thread thread = (Thread) joinPoint.getTarget();
// 通過正則檢測線程名稱
if (ThreadUtils.isDefaultThreadName(thread)) {
// 打印警告信息(線程對象和該方法調用的位置)
LogUtils.e(TAG, "發現啓動線程[" + thread + "]未自定義線程名稱! [" + joinPoint.getSourceLocation() + "]");
// 設置線程名稱,名稱拼接該方法調用處上下文this對象
thread.setName(thread.getName() + "-" + joinPoint.getThis());
}
} catch (Throwable t) {
t.printStackTrace();
}
}
}
Before表示在切入點前織入,call表示在該方法的調用處,第一個星表示不限return type,java.lang.Thread.start表示完全匹配Thread類的start方法,(…)表示不限方法參數。
該切入點會在所有調用thread.start的地方前面織入名稱檢測和設置名稱的代碼。
線程名稱檢測
若thread未設置名稱,則會使用默認名稱,可以看Thread的構造方法。
/* For autonumbering anonymous threads. */
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
public Thread() {
// 第三個參數即默認名稱
init(null, null, "Thread-" + nextThreadNum(), 0);
}
Thread在創建時會設置一個默認名稱,Thread-數字遞增,所以可以通過匹配這個名稱來判斷Thread是否設置了自定義名稱。
看ThreadUtils.isDefaultThreadName方法:
public static boolean isDefaultThreadName(Thread thread) {
String name = thread.getName();
String pattern = "^Thread-[1-9]\\d*$";
return Pattern.matches(pattern, name);
}
通過正則表達式來判斷,若完全匹配則表示當前是默認名稱。
使用示例
創建幾個Thread,分別設置名稱和不設置名稱,然後啓動運行。
public void renameThreadName(View view) {
// 未設置名稱
new Thread(new PrintNameRunnable()).start();
// 設置名稱
Thread t = new Thread(new PrintNameRunnable());
t.setName("myname-thread-test");
t.start();
}
private static class PrintNameRunnable implements Runnable {
@Override
public void run() {
// 打印線程名稱
Log.d(TAG, "thread name: " + Thread.currentThread().getName());
}
}
運行後查看logcat輸出日誌:
可以看到檢測到一個線程啓動時未設置自定義名稱,並且打印出該方法調用位置。
當線程啓動後,在Runnable中打印當前線程名稱,可以看到線程名稱已經被設置,並且可以知道thread啓動所在上下文。
工信部檢查
工信部發文要求APP在用戶未同意隱私協議之前,不得收集用戶、設備相關信息,例如imei、device id、設備已安裝應用列表、通訊錄等能夠唯一標識用戶和用戶設備隱私相關的信息。
注意,這裏的用戶同意隱私協議不同於APP權限申請,是屬於業務層面上的隱私協議。若用戶未同意隱私協議,即使在系統應用設置中打開該APP的所有權限,業務代碼中也不能獲取相關信息。
如圖,必須用戶同意後,業務代碼中才能獲取需要的信息。
痛點
要對代碼中所有涉及隱私信息獲取的地方做檢查,容易疏漏。萬一出現遺漏,將面臨工信部的下架整改處罰。而且部分三方SDK中沒有嚴格按照工信部要求,會私自進行用戶、設備相關信息的獲取。
解決
可以在所有調用隱私信息API的地方前面織入檢查代碼,一舉涵蓋自身業務代碼和三方SDK代碼進行攔截。
注意,通過動態加載的代碼中的調用行爲和native層中的行爲無法徹底攔截。
攔截API直接調用
@Aspect
public class PrivacyAspect {
// 攔截獲取手機安裝應用列表信息的調用
private static final String POINT_CUT_GET_INSTALLED_APPLICATION = "call (* android.content.pm.PackageManager.getInstalledApplications(..))";
private static final String POINT_CUT_GET_INSTALLED_PACKAGES = "call (* android.content.pm.PackageManager.getInstalledPackages(..))";
// 攔截獲取imei、device id的調用
private static final String POINT_CUT_GET_IMEI = "call (* android.telephony.TelephonyManager.getImei(..))";
private static final String POINT_CUT_GET_DEVICE_ID = "call(* android.telephony.TelephonyManager.getDeviceId(..))";
// 攔截getLine1Number方法的調用
private static final String POINT_CUT_GET_LINE_NUMBER = "call (* android.telephony.TelephonyManager.getLine1Number(..))";
// 攔截定位的調用
private static final String POINT_CUT_GET_LAST_KNOWN_LOCATION = "call (* android.location.LocationManager.getLastKnownLocation(..))";
private static final String POINT_CUT_REQUEST_LOCATION_UPDATES = "call (* android.location.LocationManager.requestLocationUpdates(..))";
private static final String POINT_CUT_REQUEST_LOCATION_SINGLE = "call (* android.location.LocationManager.requestSingleUpdate(..))";
// ···
@Around(POINT_CUT_GET_INSTALLED_APPLICATION)
public Object callGetInstalledApplications(ProceedingJoinPoint joinPoint) {
return handleProceedingJoinPoint(joinPoint, new ArrayList<ApplicationInfo>());
}
@Around(POINT_CUT_GET_INSTALLED_PACKAGES)
public Object callGetInstalledPackages(ProceedingJoinPoint joinPoint) {
return handleProceedingJoinPoint(joinPoint, new ArrayList<PackageInfo>());
}
@Around(POINT_CUT_GET_IMEI)
public Object callGetImei(ProceedingJoinPoint joinPoint) {
return handleProceedingJoinPoint(joinPoint, "");
}
@Around(POINT_CUT_GET_DEVICE_ID)
public Object callGetDeviceId(ProceedingJoinPoint joinPoint) {
return handleProceedingJoinPoint(joinPoint, "");
}
@Around(POINT_CUT_GET_LINE_NUMBER)
public Object callGetLine1Number(ProceedingJoinPoint joinPoint) {
return handleProceedingJoinPoint(joinPoint, "");
}
@Around(POINT_CUT_GET_LAST_KNOWN_LOCATION)
public Object callGetLastKnownLocation(ProceedingJoinPoint joinPoint) {
return handleProceedingJoinPoint(joinPoint, null);
}
@Around(POINT_CUT_REQUEST_LOCATION_UPDATES)
public void callRequestLocationUpdates(ProceedingJoinPoint joinPoint) {
handleProceedingJoinPoint(joinPoint, null);
}
@Around(POINT_CUT_REQUEST_LOCATION_SINGLE)
public void callRequestSingleUpdate(ProceedingJoinPoint joinPoint) {
handleProceedingJoinPoint(joinPoint, null);
}
// ···
}
定義一個切面PrivacyAspect,和需要檢查調用的方法的切入點。其中使用Around替換對敏感API的調用的代碼,調用handleProceedingJoinPoint處理,第一個參數是連接點ProceedingJoinPoint,第二個參數是默認返回值(若原方法有返回值,則會返回結果)。
接着進入handleProceedingJoinPoint方法:
private Object handleProceedingJoinPoint(ProceedingJoinPoint joinPoint, Object fakeResult) {
if (!PrivacyController.isUserAllowed()) {
// 若用戶未同意
StringBuilder sb = new StringBuilder();
// 打印調用的方法和該調用所在位置
sb.append("用戶未同意時執行了").append(joinPoint.getSignature().toShortString())
.append(" [").append(joinPoint.getSourceLocation()).append("]");
LogUtils.e(TAG, sb.toString());
// 返回一個空的默認值
return fakeResult;
}
try {
// 執行原方法,返回原結果
return joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return fakeResult;
}
該方法中判斷用戶是否同意。若未同意,則返回空的返回值。否則放行,調用原方法。
攔截API反射調用
部分三方SDK中會通過反射調用敏感API,並且對方法名稱字符串做加密處理,以繞過靜態檢查,因此也需要對反射調用進行攔截。
@Aspect
public class PrivacyAspect {
// 攔截反射的調用
private static final String POINT_CUT_METHOD_INVOKE = "call (* java.lang.reflect.Method.invoke(..))";
// 反射方法黑名單
private static final List<String> REFLECT_METHOD_BLACKLIST = Arrays.asList(
"getInstalledApplications",
"getInstalledPackages",
"getImei",
"getDeviceId",
"getLine1Number",
"getLastKnownLocation",
"loadClass"
);
@Around(POINT_CUT_METHOD_INVOKE)
public Object callReflectInvoke(ProceedingJoinPoint joinPoint) {
// 獲取該連接點調用的方法名稱
String methodName = ((Method) joinPoint.getTarget()).getName();
if (REFLECT_METHOD_BLACKLIST.contains(methodName)) {
// 若是黑名單中的方法,則進行檢查
return handleProceedingJoinPoint(joinPoint, null);
}
try {
// 執行原方法,返回原結果
return joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return null;
}
}
通過攔截Method.invoke的調用,判斷反射調用的方法是不是黑名單中的方法。
攔截動態加載的調用
@Aspect
public class PrivacyAspect {
// 攔截加載類的調用
private static final String POINT_CUT_DEX_FIND_CLASS = "call (* java.lang.ClassLoader.loadClass(..))";
@Around(POINT_CUT_DEX_FIND_CLASS)
public Object callLoadClass(ProceedingJoinPoint joinPoint) {
Object result = null;
try {
result = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
// 打印該連接點的相關信息
StringBuilder sb = new StringBuilder();
sb.append(joinPoint.getThis()).append("中動態加載");
Object[] args = joinPoint.getArgs();
if (args != null && args.length > 0) {
sb.append("\"").append(args[0]).append("\"");
}
sb.append("得到").append(result);
sb.append(" ").append(joinPoint.getSourceLocation());
LogUtils.w(TAG, sb.toString());
return result;
}
}
攔截到loadClass後,打印日誌輸出調用處的位置。
使用示例
public void interceptPrivacy(View view) {
Log.d(TAG, "用戶同意: " + PrivacyController.isUserAllowed());
// 獲取手機安裝應用信息
List<ApplicationInfo> applicationInfos = DeviceUtils.getInstalledApplications(this);
if (applicationInfos != null && applicationInfos.size() > 5) {
applicationInfos = applicationInfos.subList(0, 5);
}
Log.d(TAG, "getInstalledApplications: " + applicationInfos);
// 獲取手機安裝應用信息
List<PackageInfo> packageInfos = DeviceUtils.getInstalledPackages(this);
if (packageInfos != null && packageInfos.size() > 5) {
packageInfos = packageInfos.subList(0, 5);
}
Log.d(TAG, "getInstalledPackages: " + packageInfos);
// 獲取imei
Log.d(TAG, "getImei: " + DeviceUtils.getImeiValue(this));
// 獲取電話號碼
Log.d(TAG, "getLine1Number: " + DeviceUtils.getLine1Number(this));
// 獲取定位信息
Log.d(TAG, "getLastKnownLocation: " + DeviceUtils.getLastKnownLocation(this));
try {
// 加載一個類
Log.d(TAG, "loadClass: " + getClassLoader().loadClass("com.cdh.aop.sample.op.BaseOp"));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
try {
// 通過反射獲取手機安裝應用信息
PackageManager pm = getPackageManager();
Method method = PackageManager.class.getDeclaredMethod("getInstalledApplications", int.class);
List<ApplicationInfo> list = (List<ApplicationInfo>) method.invoke(pm, 0);
if (list != null && list.size() > 5) {
list = list.subList(0, 5);
}
Log.d(TAG, "reflect getInstalledApplications: " + list);
} catch (Exception e) {
e.printStackTrace();
}
}
運行後查看logcat輸出日誌:
打印了敏感API調用的警告信息和調用處所在位置。
調用方最終獲取到的都是空值。
尾聲
在集成AspectJX框架打包apk後可能會遇到ClassNotFoundException,反編譯apk發現很多類沒有打進去,甚至包括Application。絕大部分原因是因爲依賴的三方庫中使用了AspectJ框架導致的衝突,或者是自己寫的切入點的語法有錯誤,或織入代碼有問題,例如方法返回值沒有對應上,或者對同一個切入點定義了有衝突的通知。若發生錯誤,會在build中顯示錯誤信息。
如果不用AOP思想和AspectJ框架實現上面的需求,會有很多繁瑣的工作量。這裏通過幾個簡單場景的應用,可以發現若能深入理解AOP思想和掌握AspectJ使用,會對架構設計和開發效率有很大的提升和幫助。
文中示例完整源碼見Efficiency-Toys