Android preloads View

Posted by likethegoddess on Fri, 28 Jan 2022 10:27:55 +0100

Why preload the View? Improve the startup speed of Activity and avoid parsing xml files every time.

My idea is to preload and cache the layout of setContentView for each Activity. The next time you open the Activity again, you can reuse the previously loaded.

Then there is a problem. We all know that each View will hold a reference to a Context. Normally, this Context is the Activity of our current page. If we cache the View of the whole page, there will be a memory leak. Yes, so you can consider using the Application object to load the View. There is another place to pay attention to. Let's talk about it. The following is the implementation details.

First, we also have two schemes for preloading View. The first is to load directly in the sub thread in the Application, that is, preload. The second is to wait until we actually open the page to load and then cache it. Therefore, enumeration is defined.

public enum ViewMode {
    PreLoad,
    LazyLoad
}

PreLoad is PreLoad and LazyLoad is lazy load.

Then, the annotation class is defined to collect all activities that need to be preloaded and obtain the layout corresponding to each Activity.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface LoadLayout {
    String value();
}

This is the usage of the annotation processor. After adding the annotation, there is no need to call setContentView again in onCreate.

@LoadLayout("activity_main")
public class MainActivity extends AppCompatActivity {}

The annotation processor is defined to process the annotation.

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;
    }

After the annotation processor finishes processing, it will be in build\generated\ap_generated_sources\debug\out \ generate a java file named Preload under this path. The content is as follows. It is very simple to record the Activity annotated. Originally, I intended to traverse all class files during framework initialization. Considering the performance problem, I used the annotation processor.

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");
  }
}

Then the preload manager class, singleton mode, is defined. Next, judge whether to initialize the ViewMode passed in during initialization.

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();
            }
        }

In the preload mode, the probability information is obtained from the java file generated by the annotation processor through reflection. Then initialize.

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;
    }

Get the id of layout through resource, and get the layout through layouyinfrater. It should be noted that our layouyinfrater uses the global context Application object. After this operation, we will get the views of all activities.

Then you can use it. Register the lifecycle callback function of the Activity during initialization.

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);
            }
        });

In onActivityCreated, we get the corresponding layout through the name of the Activity. If not, get it again by lazy loading. Call setContentView to set it. It should be noted that we need to remove our cached views when onActivityDestroyed. Because there is an mParent object inside each view, which is actually the parent layout of the view. If you do not remove yourself, it will cause a memory leak.

Topics: Android Optimize