为什么要对View进行预加载呢?提高Activity的启动速度,避免每次解析xml文件。
我的思路是对每个Activity要setContentView的的布局进行预加载并且进行缓存。下次再次打开该Activity的时候直接复用之前加载过的。
那么这里面就有一个问题,我们都知道每个View都会持有一个Context的引用,正常情况下这个Context就是我们当前页面的Activity。如果我们对整个页面的View进行缓存的话不就会内存泄漏吗。是的,所以可以考虑使用Application对象去加载View。这里面还有个地方需要注意,讲到再说。下面是实现方案细节。
首先,我们预加载View也有两种方案。第一种是,直接在Application中的子线程中加载,也就是预加载。第二种是等到我们真正打开页面的时候去加载然后进行缓存。所以定义了枚举。
public enum ViewMode {
PreLoad,
LazyLoad
}
PreLoad就是预加载,LazyLoad就是懒加载。
然后定义了注解类用于收集所有需要进行预加载布局的Activity以及获取每个Activity对应的布局。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface LoadLayout {
String value();
}
这是该注解处理器的用法,加了该注解后就不需要再在onCreate中调用setContentView了。
@LoadLayout("activity_main")
public class MainActivity extends AppCompatActivity {}
定义了注解处理器对该注解进行处理。
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
CodeBlock.Builder builder = CodeBlock.builder();
Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(LoadLayout.class);
if (elementsAnnotatedWith == null){
return false;
}
for (Element element : elementsAnnotatedWith) {
String className = element.getEnclosingElement().toString() + "." + element.getSimpleName().toString();
LoadLayout loadMode = element.getAnnotation(LoadLayout.class);
if (loadMode == null){
continue;
}
String layoutName = loadMode.value();
builder.addStatement(
"layouts.put($S,$S)", className, layoutName
);
}
TypeSpec typeSpec = TypeSpec.classBuilder("Preload")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addField(FieldSpec.builder(Map.class, "layouts", Modifier.PUBLIC, Modifier.STATIC).initializer("new $T()", HashMap.class).build())
.addStaticBlock(builder.build())
.build();
JavaFile javaFile = JavaFile.builder("com.example.lib", typeSpec)
.build();
try {
javaFile.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
注解处理器处理完后会在build\generated\ap_generated_sources\debug\out\该路径下生成名为Preload的java文件。内容如下,很简单就是对进行了该注解的Activity进行了记录,原本我是打算在框架初始化的时候遍历所有的class文件去找的,考虑到性能问题,使用了注解处理器的方式。
public final class Preload {
public static Map layouts = new HashMap();
static {
layouts.put("com.example.myapplication4.MainActivity","activity_main");
layouts.put("com.example.myapplication4.SecondActivity","activity_second");
}
}
然后定义了预加载管理器类,单例模式。接下来判断初始化的时候传入的ViewMode进行初始化。
if (viewMode == ViewMode.PreLoad) {
try {
Class<?> preLoadClass = Class.forName("com.example.lib.Preload");
Field layouts = preLoadClass.getField("layouts");
layoutIds = (Map<String, String>) layouts.get(null);
initAllClasses();
} catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
如果是预加载模式的话通过反射获取到注解处理器生成的java文件获取几率信息。然后初始化。
private void initAllClasses() {
for (String kclass : layoutIds.keySet()) {
try {
Class<?> cls = Class.forName(kclass);
getRootView(cls);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
private View getRootView(Class<?> cls) {
if (Activity.class.isAssignableFrom(cls)) {
LoadLayout loadMode = cls.getAnnotation(LoadLayout.class);
if (loadMode == null) {
return null;
}
String layout = loadMode.value();
int layoutId = app.getResources().getIdentifier(layout, "layout", app.getPackageName());
if (layoutInflater == null) {
layoutInflater = LayoutInflater.from(app);
}
View root = layoutInflater.inflate(layoutId, null);
activityRoots.put(cls.getName(), root);
Log.e(TAG, "getRootView: " + cls.getName());
return root;
}
return null;
}
通过resource拿到layout的id,通过LayouyInflater获取到布局。需要注意的就是我们的LayouyInflater使用 的是全局上下文Application对象。这样操作下来我们便获取到了所有的Activity的视图了。
然后就可以进行使用了。在初始化的时候注册Activity的生命周期回调函数。
app.registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacksAdapter() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
String actName;
if (activityRoots.containsKey(actName = activity.getClass().getName())) {
View view = activityRoots.get(actName);
ViewGroup parent = (ViewGroup) view.getParent();
if (parent != null) {
parent.removeView(view);
}
activity.setContentView(view);
} else {
View rootView = getRootView(activity.getClass());
if (rootView == null) {
return;
}
ViewGroup parent = (ViewGroup) rootView.getParent();
if (parent != null) {
parent.removeView(rootView);
}
activity.setContentView(rootView);
}
}
@Override
public void onActivityDestroyed(Activity activity) {
View view = activityRoots.get(activity.getClass().getName());
ViewGroup root = activity.findViewById(android.R.id.content);
root.removeView(view);
}
});
在onActivityCreated中我们通过Activity的name去获取对应的布局。没有的话就按照懒加载的方式再次去获取。调用setContentView设置进去。需要注意的是我们需要在onActivityDestroyed的时候将我们缓存的视图移除掉。因为每个View的内部有个mParent对象,该对象其实就是view的父布局。如果不对自身进行remove的话就是造成内存泄漏。
2021.6.23
想起来这个方案的一个内存泄漏的问题。比如一个ImageView引用了Bitmap,这样就会导致Bitmap的内存得不到释放。remove的时候找到所有的Imageview调用他们的setImageDrawable(null).