關於AOP思想和AspectJX框架你們都耳熟能詳,AspectJ爲開發者提供了實現AOP的基礎能力,能夠經過它來實現符合各自業務需求的功能。java
這裏藉助AspectJX框架來實現效能提高相關的一些有意思的功能,AspectJX框架的配置和使用在README中有詳細步驟,也能夠參考官方demo。android
AspectJ中的語法說明詳見: github.com/hiphonezhu/… github.com/HujiangTech…git
平常開發中,常常會在某個關鍵方法中打印Log輸出一段字符串和參數變量的值來進行分析調試,或者在方法執行先後打印Log來查看方法執行的耗時。github
若是須要在業務主流程中的多個關鍵方法中增長日誌,查看方法執行的輸入參數和返回結果是否正確,只能繁瑣的在每一個方法開頭添加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.javaapp
自定義一個註解AutoLog,用於給想要打印日誌的方法作標記。框架
@Aspect
public class LogAspect {
/** * 切入點,添加了AutoLog註解的全部方法體內 */
@Pointcut("execution (@com.cdh.aop.toys.annotation.AutoLog * *(..))")
public void logMethodExecute() {
}
// Advice ···
}
複製代碼
建立一個日誌切面LogAspect,在其中定義一個切入點,對全部添加了AutoLog註解的方法進行代碼織入。dom
切入點中的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輸出日誌:
能夠看到第一個方法已經切換到子線程中運行,第2、三個方法又運行在主線程中,第四個方法又運行在子線程中。
一般咱們在建立使用Thread時,須要給它設置一個名稱,便於分析和定位該Thread所屬業務模塊。
開發過程當中出現疏漏或者引入的第三方庫中不規範使用線程,例如直接建立線程運行,或者匿名線程等。當想要分析線程時,就會看到不少Thread-一、二、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輸出日誌:
工信部發文要求APP在用戶未贊成隱私協議以前,不得收集用戶、設備相關信息,例如imei、device id、設備已安裝應用列表、通信錄等可以惟一標識用戶和用戶設備隱私相關的信息。
注意,這裏的用戶贊成隱私協議不一樣於APP權限申請,是屬於業務層面上的隱私協議。若用戶未贊成隱私協議,即便在系統應用設置中打開該APP的全部權限,業務代碼中也不能獲取相關信息。
如圖,必須用戶贊成後,業務代碼中才能獲取須要的信息。
要對代碼中全部涉及隱私信息獲取的地方作檢查,容易疏漏。萬一出現遺漏,將面臨工信部的下架整改處罰。並且部分三方SDK中沒有嚴格按照工信部要求,會私自進行用戶、設備相關信息的獲取。
能夠在全部調用隱私信息API的地方前面織入檢查代碼,一舉涵蓋自身業務代碼和三方SDK代碼進行攔截。
注意,經過動態加載的代碼中的調用行爲和native層中的行爲沒法完全攔截。
@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;
}
複製代碼
該方法中判斷用戶是否贊成。若未贊成,則返回空的返回值。不然放行,調用原方法。
部分三方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輸出日誌:
在集成AspectJX框架打包apk後可能會遇到ClassNotFoundException,反編譯apk發現不少類沒有打進去,甚至包括Application。絕大部分緣由是由於依賴的三方庫中使用了AspectJ框架致使的衝突,或者是本身寫的切入點的語法有錯誤,或織入代碼有問題,例如方法返回值沒有對應上,或者對同一個切入點定義了有衝突的通知。若發生錯誤,會在build中顯示錯誤信息。
若是不用AOP思想和AspectJ框架實現上面的需求,會有不少繁瑣的工做量。這裏經過幾個簡單場景的應用,能夠發現若能深刻理解AOP思想和掌握AspectJ使用,會對架構設計和開發效率有很大的提高和幫助。
文中示例完整源碼見Efficiency-Toys