MVP模式探索——Presenter和View解耦的嘗試之APT技術
前言
在前一篇文章《MVP模式探索-Presenter和View解耦的嘗試》裡,我在文章末尾說到了該解耦方式有幾個已知的問題,其中一個就是用反射的方式去執行方法會有效能上的損耗,但是可以用APT技術替代反射的方式。後來我去查了一下反射效能損耗的問題,發現反射出現效能損耗是有一定條件的,那就是需要在反覆多次執行同一個反射呼叫時,當反覆執行的次數達到一個數量級後,才會和普通呼叫產生效能差異。然而像MMVP模式裡的那些反射執行的方法,並不會在短時間內反覆多次呼叫,所以其實並不存在什麼效能上的問題。
關於反射效能問題的文章參考如下
但是今天在這裡我還是要用APT技術去替代反射的方案,這僅僅是多嘗試一種方式和思維去解決同一個問題,也順便學習一下APT技術和JavaPoet。
當然,在具體實施之前,如果大家對APT技術或JavaPoet還不太瞭解,就先去充一下電在來看,會順暢很多。
一、技術實施細節
因為替代方案是在原來的程式碼基礎上改的,所以後面的敘述都會以原來的程式碼為相對參考,所以,如果有什麼疑惑的地方,請記得去看我前一篇文章
1.1 基本說明
在嘗試APT技術的時候,一開始就遇到了一個問題,我們要編寫的annotationProcessor是一個Java Library,而我最開始的MMVP moudle是一個Android Library,annotationProcessor需要MMVP moudle裡的註解,但是我發現似乎沒有辦法把一個Android Library作為Java Library的依賴,但是反過來卻是可以的,所以我就只好把MMVP moudle裡的一些annotationProcessor需要的註解抽離出去,單獨建了一個mmvpannotation Java Library,讓annotationProcessor moudle和MMVP moudle都依賴mmvpannotation。關於把Android Library作為Java Library的依賴的資料參考
原來在使用反射方式的的時候,有些類(比如Presenter類)是不需要使用註解來標識的,但是使用APT技術後,只能通過註解來找到目標類生成對應的代理類解析MMVPAction後,再呼叫目標類的方法或修改其屬性,所這裡就新增了了一個註解,MMVPActionProcessor,這個註解在mmvpannotation moudle下
/** * 包名:com.ykbjson.lib.mmvp.annotation * 描述:MMVPAction處理類需要的註解 * 建立者:yankebin * 日期:2018/5/7 */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public @interface MMVPActionProcessor { }
在MMVP專案裡,APT技術在編譯時生成的類,需要能夠直接呼叫或通過目標類的例項呼叫目標類(即添加了MMVPActionProcessor註解的類)的某些方法或者更改目標類的某些屬性(這就是和反射的差別之處,直接呼叫),因為IMMVPActionHandler介面是專門負責處理MMVPActin的介面,所以我們就讓編譯時生成的類也實現IMMVPActionHandler介面,方便處理MMVPAction後呼叫目標類的方法。
修改之後的專案結構有所變化,新增了mmvpannotation、mmvpcompiler兩個moudle
1.2 APT+JavaPoet輸出目標類
其實這裡真沒什麼好說的,只要大家去看了我前面說的那幾篇文章(APT+JavaPoet),加上我程式碼裡的註釋,一定很輕易地就可以看懂了。
我們來看一下這個生成目標類的MMVPAnnotationProcessor
/**
* 包名:com.ykbjson.lib.mmvp.compiler
* 描述:mmvp{@link MMVPActionProcessor}註解處理器
* 建立者:yankebin
* 日期:2018/5/4
*/
@AutoService(Processor.class)
public class MMVPAnnotationProcessor extends AbstractProcessor {
private static final String CLASS_NAME_SUFFIX = "_MMVPActionProcessor";
private static final String METHOD_NAME_SUFFIX = "_Process";
private static final String OVERRIDE_METHOD_NAME_HANDLE_ACTION = "handleAction";
private static final String OVERRIDE_METHOD_NAME_GET = "get";
private Elements elementUtils;
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
parseExecutor(roundEnv);
return true;
}
private void parseExecutor(RoundEnvironment roundEnv) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(MMVPActionProcessor.class);
for (Element element : elements) {
// 判斷是否Class
TypeElement typeElement = (TypeElement) element;
//要生成的類
TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder(element.getSimpleName() +
CLASS_NAME_SUFFIX)//類名
.addSuperinterface(ClassName.get("com.ykbjson.lib.mmvp", "IMMVPActionHandler"))//實現的介面
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)//類修飾符
.addField(FieldSpec.builder(TypeName.get(typeElement.asType()), "target", Modifier.PRIVATE).build());//成員變數
//生成的類的構造方法
MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)//方法的修飾符
.addParameter(TypeName.get(typeElement.asType()), "target")//方法的引數
.addStatement("this.$N = $N", "target", "target");//方法的內容
typeSpecBuilder.addMethod(constructorBuilder.build());//新增到類裡
//target類目標方法獲取和引數解析後呼叫。這裡就是遍歷target目標類裡的方法,看那哪些方法添加了ActionProcess註解,
// 然後,根據收到的Action,根據方法的註解引數,解析action裡的引數,然後呼叫目標類的方法。
final List<? extends Element> members = elementUtils.getAllMembers(typeElement);
final Map<String, String> methodMap = new LinkedHashMap<>();
for (Element item : members) {
ActionProcess methodAnnotation = item.getAnnotation(ActionProcess.class);
if (methodAnnotation == null) {
continue;
}
final String generatedMethodName = item.getSimpleName().toString() + METHOD_NAME_SUFFIX;
//儲存方法和註解的關係
methodMap.put(methodAnnotation.value(), generatedMethodName);
MethodSpec.Builder actionProcessMethodSpecBuilder = MethodSpec.methodBuilder(
generatedMethodName)
.returns(TypeName.BOOLEAN)
.addModifiers(Modifier.PUBLIC);
//方法必要的唯一引數-MMVPAction
actionProcessMethodSpecBuilder.addParameter(ParameterSpec.builder(
ClassName.get("com.ykbjson.lib.mmvp", "MMVPAction"), "action")
.build());
//如果當前傳入的MMVPAction要執行的方法和當前方法不一致,中斷執行
CodeBlock codeBlock = CodeBlock.builder().beginControlFlow("if(!\"" + methodAnnotation.value() +
"\".equals(action.getAction().getAction()))")
.addStatement("return false")
.endControlFlow()
.build();
actionProcessMethodSpecBuilder.addCode(
codeBlock
);
//獲取和處理方法引數列表
ExecutableElement method = (ExecutableElement) item;//方法
List<? extends VariableElement> parameters = method.getParameters();//方法的引數
StringBuilder parametersBuffer = new StringBuilder();
//引數集合,需要引數才去解析引數,這下面生成的程式碼和MMVPArtist裡的execute方法裡的程式碼非常類似
if (!parameters.isEmpty()) {
actionProcessMethodSpecBuilder.addStatement("$T paramList= new ArrayList<>()", ArrayList.class);
if (methodAnnotation.needActionParam()) {
if (methodAnnotation.needTransformAction()) {
actionProcessMethodSpecBuilder.addStatement(
"action = action.transform()"
);
}
actionProcessMethodSpecBuilder.addStatement(
"paramList.add(action)"
);
}
if (methodAnnotation.needActionParams()) {
actionProcessMethodSpecBuilder.addStatement(
//這裡其實可以使用JavaPoet的beginControlFlow來優雅地實現for迴圈
"if(null != action.getParams() && !action.getParams().isEmpty()) {\n" +
"for (String key : action.getParams().keySet()) {\n" +
" paramList.add(action.getParam(key));\n" +
"}" +
"}"
);
}
for (int i = 0; i < parameters.size(); i++) {
parametersBuffer.append(
"(" + parameters.get(i).asType() + ")" + "paramList.get(" + i + ")");
if (i != parameters.size() - 1) {
parametersBuffer.append(",");
}
}
}
//這裡生成的程式碼類似 target.findViewById(-122443433)
actionProcessMethodSpecBuilder.addStatement("target." +
method.getSimpleName().toString() +
"(" + parametersBuffer.toString() + ")");
actionProcessMethodSpecBuilder.addStatement("return true");
typeSpecBuilder.addMethod(actionProcessMethodSpecBuilder.build());
}
//過載IMMVPActionHandler方法handleAction
MethodSpec.Builder overrideMethodSpecBuilder = MethodSpec.methodBuilder(OVERRIDE_METHOD_NAME_HANDLE_ACTION)
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.BOOLEAN);
overrideMethodSpecBuilder.addParameter(ParameterSpec.builder(
ClassName.get("com.ykbjson.lib.mmvp", "MMVPAction"), "action")
.addAnnotation(ClassName.get("android.support.annotation", "NonNull"))
.build());
//由於無法預知即將呼叫的方法,只能把重寫的方法全部執行一遍,重寫方法裡有判斷可以避免錯誤執行,並且只要有某個方法返回了true,後續方法將不再執行.
// 或許這樣也不比反射執行的風險小吧.
int index = 0;
StringBuilder resultBuilder = new StringBuilder();
for (String key : methodMap.keySet()) {
resultBuilder.append(methodMap.get(key) + "(action)" + (index != methodMap.keySet().size() - 1 ? "||" : ""));
index++;
}
overrideMethodSpecBuilder.addStatement("return " + (resultBuilder.length() == 0 ? "false" : resultBuilder.toString()));
typeSpecBuilder.addMethod(overrideMethodSpecBuilder.build());
//過載IMMVPActionHandler方法get
overrideMethodSpecBuilder = MethodSpec.methodBuilder(OVERRIDE_METHOD_NAME_GET)
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(ClassName.get("com.ykbjson.lib.mmvp", "IMMVPActionHandler"));
overrideMethodSpecBuilder.addStatement("return this");
typeSpecBuilder.addMethod(overrideMethodSpecBuilder.build());
//生成java檔案
JavaFile javaFile = JavaFile.builder(getPackageName(typeElement), typeSpecBuilder.build())
.addFileComment(" Generated code from MMVP. Do not modify! ")
.build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> supportedAnnotationTypes = new LinkedHashSet<>();
supportedAnnotationTypes.add(MMVPActionProcessor.class.getCanonicalName());
return supportedAnnotationTypes;
}
private String getPackageName(TypeElement type) {
return elementUtils.getPackageOf(type).getQualifiedName().toString();
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
elementUtils = processingEnv.getElementUtils();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_7;
}
}
這個類裡面如果一定要細講的話,那講得很多的就是java註解語法基礎和JavaPoet使用方法了,但是這並不是本篇文章的討論範圍,也不是大家的難點所在,所以就不贅述了,總之,這裡就是在編譯時為所有添加了MMVPActionProcessor註解的類都生成一個對應的類,生成的這個類可以直接操作添加了MMVPActionProcessor註解的類裡的方法或屬性,從而用直接呼叫的方式替代反射呼叫的方式。
二、效果測試和比對
我們以mmvpsample裡的LoginPresenter為例,看一看這個類生成的類到底是如何呼叫LoginPresenter裡的方法的。
LoginPresenter
/**
* 包名:com.ykbjson.app.hvp.login
* 描述:登入邏輯處理
* 建立者:yankebin
* 日期:2018/4/13
*/
@MMVPActionProcessor
public class LoginPresenter implements LoginContract.ILoginPresenter {
public static final String ACTION_DO_LOGIN = "LoginPresenter.action.ACTION_DO_LOGIN";
public static final String ACTION_NOTIFY_LOGIN_FAILED = "LoginPresenter.action.ACTION_NOTIFY_LOGIN_FAILED";
public static final String ACTION_NOTIFY_LOGIN_SUCCESS = "LoginPresenter.action.ACTION_NOTIFY_LOGIN_SUCCESS";
private LoginRepository repository = new LoginRepository();
@ActionProcess(value = ACTION_DO_LOGIN, needActionParam = true, needTransformAction = true)
@Override
public void doLogin(final MMVPAction action) {
//引數校驗
String mobile = action.getParam("mobile");
String password = action.getParam("password");
if (TextUtils.isEmpty(mobile)) {
action.getAction().setAction(ACTION_NOTIFY_LOGIN_FAILED);
action.clearParam().putParam("error", "登入賬號無效").send();
return;
}
if (TextUtils.isEmpty(password)) {
action.getAction().setAction(ACTION_NOTIFY_LOGIN_FAILED);
action.clearParam().putParam("error", "登入密碼無效").send();
return;
}
repository.doLogin(mobile, password, new IMMVPOnDataCallback<LoginResult>() {
@Override
public void onSuccess(LoginResult data) {
action.getAction().setAction(ACTION_NOTIFY_LOGIN_SUCCESS);
action.clearParam().putParam("loginResult", data).send();
}
@Override
public void onError(String msg) {
action.getAction().setAction(ACTION_NOTIFY_LOGIN_FAILED);
action.clearParam().putParam("error", msg).send();
}
});
}
@Override
public boolean handleAction(@NonNull MMVPAction action) {
switch (action.getAction().getAction()) {
case ACTION_DO_LOGIN:
doLogin(action.transform());
break;
}
return true;
}
@NonNull
@Override
public IMMVPActionHandler get() {
return this;
}
}
只需要在LoginPresenter上加上MMVPActionProcessor註解,rebuild project,然後到mmvpsample/build/generated/source/debug(或release,根據你build的環境)/LoginPresenter所在包路徑/目錄下,會有一個生成的類:
LoginPresenter_MMVPActionProcessor
// Generated code from MMVP. Do not modify!
package com.ykbjson.app.hvp.login;
import android.support.annotation.NonNull;
import com.ykbjson.lib.mmvp.IMMVPActionHandler;
import com.ykbjson.lib.mmvp.MMVPAction;
import java.lang.Override;
import java.util.ArrayList;
public final class LoginPresenter_MMVPActionProcessor implements IMMVPActionHandler {
private LoginPresenter target;
public LoginPresenter_MMVPActionProcessor(LoginPresenter target) {
this.target = target;
}
public boolean doLogin_Process(MMVPAction action) {
if(!"LoginPresenter.action.ACTION_DO_LOGIN".equals(action.getAction().getAction())) {
return false;
}
ArrayList paramList= new ArrayList<>();
action = action.transform();
paramList.add(action);
target.doLogin((com.ykbjson.lib.mmvp.MMVPAction)paramList.get(0));
return true;
}
@Override
public boolean handleAction(@NonNull MMVPAction action) {
return doLogin_Process(action);
}
@Override
public IMMVPActionHandler get() {
return this;
}
}
生成這個類的目的,文章開篇就說了,就是替代原來依靠反射方式執行某個方法的方案。原來MMVPArtist的處理邏輯是,在接收到一個MMVPAction後,經過層層查詢和解析,如果有符合條件的目標物件需要解析這個MMVPAction,就會執行execute方法
/**
* 執行action裡目標類需要執行的方法
*
* @param action {@link MMVPAction}
* @param find {@link MMVPView}或{@link MMVPPresenter}
* @return
*/
private static boolean execute(MMVPAction action, IMMVPActionHandler find) {
Method executeMethod = findRegisterMMVPActionMethod(action);
if (null == executeMethod) {
if (enableLog) {
Log.d(TAG, " Find " + find.getClass().getName() + "'s execute method failure");
}
return false;
}
if (enableLog) {
Log.d(TAG, " Find method " + find.getClass().getName() + "." + executeMethod.getName() + " success");
}
List<Object> paramList = new ArrayList<>();
ActionProcess methodAnnotation = executeMethod.getAnnotation(ActionProcess.class);
if (methodAnnotation.needActionParam()) {
if (methodAnnotation.needTransformAction()) {
action = action.transform();
}
paramList.add(action);
}
if (methodAnnotation.needActionParams() && null != action.getParams() && !action.getParams().isEmpty()) {
for (String key : action.getParams().keySet()) {
paramList.add(action.getParam(key));
}
}
Object[] params = paramList.isEmpty() ? null : paramList.toArray();
try {
executeMethod.setAccessible(true);
executeMethod.invoke(find, params);
if (enableLog) {
Log.d(TAG, " Execute "
+ find.getClass().getName() + "." + executeMethod.getName() + " success");
}
return true;
} catch (IllegalAccessException e) {
e.printStackTrace();
if (enableLog) {
Log.d(TAG, " Execute "
+ action.getTargetClass().getName() + "." + executeMethod.getName() + " failure", e);
}
} catch (InvocationTargetException e) {
e.printStackTrace();
if (enableLog) {
Log.d(TAG, " Execute "
+ action.getTargetClass().getName() + "." + executeMethod.getName() + " failure", e);
}
}
return false;
}
通過程式碼大家可以看到,這裡面最終目標類的方法的呼叫是使用的反射方式,現在我們用APT技術就是想不使用反射的方式也能呼叫目標類的方法。
當我們有了用APT技術生成的類時,execute方法可以修改如下,這裡是重新寫了一個方法,以示區分
/**
* 執行action裡目標類需要執行的方法
*
* @param action {@link MMVPAction}
* @param target 要執行action的類
* @return
*/
private static boolean executeByApt(@NonNull MMVPAction action, @NonNull Object target) {
IMMVPActionHandler executor = createExecutor(target);
return executor.handleAction(action);
}
是不是很好奇executor到底是誰?我們接著看一下createExecutor方法
/**
* 建立{@link IMMVPActionHandler}
*
* @param target 當前關聯的目標物件
* @return
*/
private static IMMVPActionHandler createExecutor(@NonNull Object target) {
Class<?> targetClass = target.getClass();
if (enableLog) {
Log.d(TAG, "Looking up executor for " + targetClass.getName());
}
Constructor<? extends IMMVPActionHandler> constructor = findExecutorConstructorForClass(targetClass);
if (constructor == null) {
return IMMVPActionHandler.EMPTY;
}
//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
return constructor.newInstance(target);
} catch (IllegalAccessException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InstantiationException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
throw new RuntimeException("Unable to create executor instance.", cause);
}
}
這裡根據target,通過findExecutorConstructorForClass方法,找到了 LoginPresenter_MMVPActionProcessor 的構造方法,生成了LoginPresenter_MMVPActionProcessor的例項。
/**
* 建立{@link IMMVPActionHandler}
*
* @param cls 當前關聯的目標類
* @return
*/
@Nullable
@CheckResult
@UiThread
private static Constructor<? extends IMMVPActionHandler> findExecutorConstructorForClass(Class<?> cls) {
Constructor<? extends IMMVPActionHandler> executorCtor = EXECUTORS.get(cls);
if (executorCtor != null) {
if (enableLog) {
Log.d(TAG, "Cached in executor map.");
}
return executorCtor;
}
String clsName = cls.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
if (enableLog) {
Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
}
return null;
}
try {
Class<?> executorClass = cls.getClassLoader().loadClass(clsName + "_MMVPActionProcessor");
//noinspection unchecked
executorCtor = (Constructor<? extends IMMVPActionHandler>) executorClass.getConstructor(cls);
if (enableLog) {
Log.d(TAG, "HIT: Loaded executor class and constructor.");
}
} catch (ClassNotFoundException e) {
if (enableLog) {
Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
}
executorCtor = findExecutorConstructorForClass(cls.getSuperclass());
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find executor constructor for " + clsName, e);
}
EXECUTORS.put(cls, executorCtor);
return executorCtor;
}
如果大家研究過ButterKnife的原始碼,看到這裡,是否有一種似曾相識的感覺?
現在有了LoginPresenter_MMVPActionProcessor的例項,然後呼叫其handleAction方法,我們回過去看看LoginPresenter_MMVPActionProcessor的handleAction方法,呼叫了自身的doLogin_Process方法,然後再看doLogin_Process方法,是不是呼叫了LoginPresenter的doLogin方法?你肯定會很好奇,LoginPresenter_MMVPActionProcessor怎麼會這麼智慧,直接就知道要呼叫LoginPresenter的doLogin方法,其實它一點都不智慧,如果LoginPresenter有N個方法添加了ActionProcess註解,那麼LoginPresenter_MMVPActionProcessor的handleAction方法就會是這個樣子
@Override
public boolean handleAction(@NonNull MMVPAction action) {
return method1(action)||method2(action)||method3(action)||...||methodN(action);
}
所以我在MMVPAnnotationProcessor裡有這樣一句註釋:“由於無法預知即將呼叫的方法,只能把重寫的方法全部執行一遍,重寫方法裡有判斷可以避免錯誤執行,並且只要有某個方法返回了true,後續方法將不再執行。或許這樣也不比反射執行的風險小吧”。這就是在我已經快要放棄用APT技術解決反射問題時想到的一個笨辦法,雖然解決了我遇到的問題,但是我內心是拒絕這樣去實現的。
為什麼說無法預知即將呼叫的方法?因為這些程式碼都是我主動去生成的,即便我知道這裡一定有一個handleAction方法,其引數MMVPAction,但是MMVPAction在我面前他就是一個字串,我無法得到裡面的內容,也就無法根據MMVPAction的action精準匹配要執行的方法。
當然,如果瞭解我MMVP專案的童鞋肯定會發現一個更簡單的方法,那就是我的View和Presenter都實現了IMMVPActionHandler介面的,LoginPresenter具體實現了handleAction方法,所以我們的LoginPresenter_MMVPActionProcessor的handleAction方法就可以簡化成這個樣子
@Override
public boolean handleAction(@NonNull MMVPAction action) {
return target.handleAction(action);
}
甚至連反射和這裡的APT技術都不需要,直接在MMVPArtist的handleActionFromView或handleActionFromPresenter方法裡,直接呼叫IMMVPActionHandler的handleAction方法。只是這樣做的前提是,Presenter和View都必須正確實現handleAction方法。
關於用APT技術解決反射呼叫效能損耗的問題,以及對專案的改造,大概就是這個樣子啦,如果看文章覺得不清楚很晦澀,那就去文章末尾貼出的專案地址檢視原始碼,或許會好一些吧。如果大家有什麼意見或建議,希望各位不吝賜教,感謝!
三、結語
今天呢,我們一起探討了用APT技術解決反射呼叫效能損耗的問題,雖然結果並不完美,甚至可以說的上是有點牽強,但是,這都不重要,重要的是我們又學會了用一些不同的技術解決相同的問題,用不同的思維方式去思考同一個問題,我想,這應該是值得的。
通過一次次的寫部落格,我發現一個道理,沒有什麼東西是一蹴而就的,你們現在看到原始碼,寥寥數行,可是我都不記得自己改了多少次,才僅僅有這樣的結果。這或許是一種磨礪,也或許是一個程式猿的樂趣吧。
最後,當然是所有程式猿都喜歡的程式碼啦