为什么要对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).