This article is originally published by the authorized personal public account “Hongyang”.

1. An overview of the

During Android development, we write various XML layout files almost every day, and then the app will convert our layout files into a View to display on the interface when running.

This transformation essentially parses the XML layout file and, based on each View tag in the XML, will:

  1. Tag name -> Name of View
  2. Various attributes -> AttributeSet object

Reflection then calls the constructor of the View’s two arguments.

This is why, when we customize a control to use in XML, we need to override its two-parameter constructor.

This design is indeed extremely scalable, but it also introduces some performance issues.

You can obviously see that some time-consuming operations are involved in the process of transferring the XML file to the View:

  1. IO operation, XML parsing;
  2. Reflection;

Especially in real projects, some page layout elements are very many, so the entire page dozens of controls may need to reflect the generation.

So a lot of times, some of the core pages, in order to speed things up, we’re going to look at code generation instead of XML, and one of the biggest problems with that is that maintainability drops dramatically.

In the interest of both maintainability and runtime efficiency, many developers think that XML is, after all, a very regular file that can be parsed into a View at compile time and retrieved at run time without IO and reflection.

Indeed, the idea is so good that there is also an open source library on Github published by IReader:

Github.com/iReaderAndr…

X2c is a very good idea and basically solves the two time-consuming issues I mentioned above, but introduces new ones, namely compatibility and stability.

In addition, X2C generated code uses APT, which is only for this module to do some things, involving complex inter-module dependencies, so there will be a lot of problems. X2c should also do a lot of processing in APT, but when many projects do various compilation optimizations during compilation, There’s gonna be some sparks.

This article will also cover APT, because it does not involve resources, it also encounters some problems, which will be described below.

Of course, if x2C can be introduced and self-maintained, it is actually quite good. I support this scheme very much, but it has certain risks.

Note: this article does not discuss which one is the best, blog is more for learning, the key is to absorb the knowledge points contained in each scheme, expand their usable knowledge base.

2. Take a step back

As we said earlier, the process of fully hosting XML ->View has some risks, so can we step back and look at this issue?

Since there are two time points involved in the process of transferring an XML file to a View:

  1. IO operation, XML parsing;
  2. Reflection;

XML parsing is not something to be interfered with, it seems like a high risk thing to leave to Google, and there is some caching logic involved with XMLBlock at the bottom.

That only leaves one reflection operation. Is that a soft touch?

Do we have a way to override the launch logic?

Of course I do. I’m sure you’re all familiar with it.

If you follow this number, we wrote in 2016:

To explore the LayoutInflater setFactory

With setFactory, we can not only control the generation of views, but also change one View into another. For example, in this article, we changed TextView into Button.

Subsequent skin change, blackening some schemes are based on this.

Which means we can now:

At runtime, we take over the generation of a View, so we can remove the reflection logic for a single View

Similar code:

if ("LinearLayout".equals(name)){
    View view = new LinearLayout(context, attrs);
    return view;
}
Copy the code

However, if you’re going to have an online project that’s pretty big, you might have all kinds of custom views, like if and else.

Collect them and write them by hand?

How do YOU collect all the views used in your project?

Assuming we collect, by hand, items are generally incremental, what about subsequent views?

We can see that we face two problems:

  1. How to collect views used in XML in your project;
  2. How to ensure that the written View generation code is compatible with the normal iteration of the project;

3. Determine the plan

At this point the target has been identified.

In XML -> View, remove reflection – dependent logic

Let’s talk about how we can solve two problems:

1. How to collect views used in XML in the project;

A simple idea is to collect all the views used in XML. We can parse all the layout. XML files in the project, but the layout.

Think about it, during the creation of APK, resources should need to merge after a Task merger.

Indeed, there is, and the detailed implementation will be mentioned later.

Let’s look at the second question:

2. How to ensure that the written View generation code is compatible with the normal iteration of the project;

We can now collect all the lists of views used, so for this:

if ("LinearLayout".equals(name)){
    View view = new LinearLayout(context, attrs);
    return view;
}
Copy the code

With regular and simple logic, it is completely possible to generate a code class at compile time to complete the generation of relevant transformation code. Apt is selected here.

With a code class for XML -> View transformation logic, the last thing you need to do is inject it at run time using LayoutFactory.

3. Find a safe injection logic

We all know that our View generation logic is in the code below the LayoutInflater:

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {
  	 // ...
    View view;
    if(mFactory2 ! = null) { view = mFactory2.onCreateView(parent, name, context, attrs); }else if(mFactory ! = null) { view = mFactory.onCreateView(name, context, attrs); }else {
        view = null;
    }

    if(view == null && mPrivateFactory ! = null) { view = mPrivateFactory.onCreateView(parent, name, context, attrs); }if (view == null) {
        final Object lastContext = mConstructorArgs[0];
        mConstructorArgs[0] = context;
        try {
            if (-1 == name.indexOf('. ')) {
                view = onCreateView(parent, name, attrs);
            } else{ view = createView(name, null, attrs); } } finally { mConstructorArgs[0] = lastContext; }}return view;
   
}
Copy the code

The View goes through mFactory2, mFactory,mPrivateFactory, and if it doesn’t finish building, it’s reflected.

TextView-> AppCompatTextView.

So we’re thinking about using mPrivateFactory, and the nice thing about using mPrivateFactory is, in the current version, mPrivateFactory is an Activity, so we just duplicate Activivity’s onCreateView:

There is no need for hooks, no interference with appCompat-related generation logic, and zero risk.

4. Start implementation

1. Get the list of control names used in the project

I created a new project, and I wrote some custom controls called MyMainView1, MyMainView, MyMainView3, MyMainView4 all declared in a Layout file, not a layout file.

As mentioned earlier, we need to find the right injection point to do this during apK construction.

When will resources be merged during apK?

Let’s print out all the tasks during the build and type:

./gradlew  app:assembleDebug --console=plain
Copy the code

Output:

>Task :app:preBuild UP-TO-DATE > Task :app:preDebugBuild UP-TO-DATE > Task :app:checkDebugManifest UP-TO-DATE > Task :app:generateDebugBuildConfig UP-TO-DATE > Task :app:javaPreCompileDebug UP-TO-DATE > Task :app:mainApkListPersistenceDebug UP-TO-DATE > Task :app:generateDebugResValues UP-TO-DATE > Task :app:createDebugCompatibleScreenManifests UP-TO-DATE > Task :app:mergeDebugShaders UP-TO-DATE > Task :app:compileDebugShaders UP-TO-DATE > Task :app:generateDebugAssets UP-TO-DATE > Task :app:compileDebugAidl NO-SOURCE > Task :app:compileDebugRenderscript NO-SOURCE > Task :app:generateDebugResources UP-TO-DATE > Task :app:mergeDebugResources UP-TO-DATE > Task :app:processDebugManifest UP-TO-DATE > Task :app:processDebugResources UP-TO-DATE > Task :app:compileDebugJavaWithJavac UP-TO-DATE > Task :app:compileDebugSources UP-TO-DATE > Task :app:mergeDebugAssets UP-TO-DATE > Task :app:processDebugJavaRes NO-SOURCE > Task :app:mergeDebugJavaResource UP-TO-DATE  > Task :app:transformClassesWithDexBuilderForDebug UP-TO-DATE > Task :app:checkDebugDuplicateClasses UP-TO-DATE > Task :app:validateSigningDebug UP-TO-DATE > Task :app:mergeExtDexDebug UP-TO-DATE > Task :app:mergeDexDebug UP-TO-DATE > Task  :app:signingConfigWriterDebug UP-TO-DATE > Task :app:mergeDebugJniLibFolders UP-TO-DATE > Task :app:mergeDebugNativeLibs UP-TO-DATE > Task :app:stripDebugDebugSymbols UP-TO-DATE > Task :app:packageDebug UP-TO-DATE >  Task :app:assembleDebug UP-TO-DATECopy the code

Which one is the most similar? There was a Task called mergeDebugResources, and that was it.

There is also a mergeDebugResources directory corresponding to the build directory:

Notice that there is a merger. XML that contains the merged contents of all the resources for the entire project.

Let’s take a look:

Pay attention to the related tags of type=layout inside.

<file name="activity_main1"
                path="/Users/zhanghongyang/work/TestViewOpt/app/src/main/res/layout/activity_main1.xml"
                qualifiers="" type="layout" />
Copy the code

We can see the circuit that contains our layout file, so we can just parse this merger. XML, and then find all the tags of type=layout inside, and then parse the layout file, and then parse the corresponding layout XML, and then we can get the control name.

By the way, this task will be injected after mergeDebugResources.

How do you inject a task?

Very simple:

project.afterEvaluate {
    def mergeDebugResourcesTask = project.tasks.findByName("mergeDebugResources")
    if(mergeDebugResourcesTask ! = null) { def resParseDebugTask = project.tasks.create("ResParseDebugTask", ResParseTask.class)
        resParseDebugTask.isDebug = truemergeDebugResourcesTask.finalizedBy(resParseDebugTask); }}Copy the code

Root directory: view_opt.gradle

We first find the mergeDebugResources task and, after that, inject a ResParseTask task.

Then complete file parsing in ResParseTask:

class ResParseTask extends DefaultTask { File viewNameListFile boolean isDebug HashSet<String> viewSet = new HashSet<>() List<String> ignoreViewNameList = arrays.asList ("include"."fragment"."merge"."view"."DateTimeView")

    @TaskAction
    void doTask() {

        File distDir = new File(project.buildDir, "tmp_custom_views")
        if(! distDir.exists()) { distDir.mkdirs() } viewNameListFile = new File(distDir,"custom_view_final.txt")
        if (viewNameListFile.exists()) {
            viewNameListFile.delete()
        }
        viewNameListFile.createNewFile()
        viewSet.clear()
        viewSet.addAll(ignoreViewNameList)

        try {
            File resMergeFile = new File(project.buildDir, "/intermediates/incremental/merge" + (isDebug ? "Debug" : "Release") + "Resources/merger.xml")

            println("ResMergeFile:${resMergeFile.getAbsolutePath()}= = =${resMergeFile.exists()}")

            if(! resMergeFile.exists()) {return
            }

            XmlSlurper slurper = new XmlSlurper()
            GPathResult result = slurper.parse(resMergeFile)
            if(result.children() ! = null) { result.childNodes().forEachRemaining({ o ->if (o instanceof Node) {
                        parseNode(o)
                    }
                })
            }


        } catch (Throwable e) {
            e.printStackTrace()
        }

    }

    void parseNode(Node node) {
        if (node == null) {
            return
        }
        if (node.name() == "file" && node.attributes.get("type") = ="layout") {
            String layoutPath = node.attributes.get("path")
            try {
                XmlSlurper slurper = new XmlSlurper()
                GPathResult result = slurper.parse(layoutPath)

                String viewName = result.name();
                if (viewSet.add(viewName)) {
                    viewNameListFile.append("${viewName}\n")}if(result.children() ! = null) { result.childNodes().forEachRemaining({ o ->if(o instanceof Node) { parseLayoutNode(o) } }) } } catch (Throwable e) { e.printStackTrace(); }}else {
            node.childNodes().forEachRemaining({ o ->
                if (o instanceof Node) {
                    parseNode(o)
                }
            })
        }

    }

    void parseLayoutNode(Node node) {
        if (node == null) {
            return
        }
        String viewName = node.name()
        if (viewSet.add(viewName)) {
            viewNameListFile.append("${viewName}\n")}if (node.childNodes().size() <= 0) {
            return
        }
        node.childNodes().forEachRemaining({ o ->
            if (o instanceof Node) {
                parseLayoutNode(o)
            }
        })
    }

}
Copy the code

Root directory: view_opt.gradle

The code is pretty simple, just parse the merger. XML, find all the layout files, parse the XML, and export it to the build directory.

Gradle = view_opt.gradle = build.gradle = build.gradle = build.gradle = build.gradle

apply from: rootProject.file('view_opt.gradle')

Copy the code

Then we run assembleDebug again, output:

Note that we also have an ignoreViewNameList object. We filter some special tags, such as “include”, “fragment”, “merge”, “view”, which you can add according to the output.

The output is:

As you can see, is the name of the deduplicated View.

By the way, it’s easy to write Gradle scripts in Java. If you’re not familiar with the syntax, just write it in Java.

At this point we have the names of all the views we use.

2. Apt generates proxy classes

With the names of all the views used, let’s use APT to generate a proxy class and proxy methods.

To use APT, we need to create 3 new modules:

  1. ViewOptAnnotation: store annotations;
  2. ViewOptProcessor: puts annotation processor-related code;
  3. ViewOptApi: puts the relevant use API.

I will not mention the basic knowledge about Apt, this knowledge is too complicated, you can check it for yourself, and I will upload the demo to Github for you to see.

Let’s go straight to our core Processor class:

@AutoService(Processor.class)
public class ViewCreatorProcessor extends AbstractProcessor {

    private Messager mMessager;


    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mMessager = processingEnv.getMessager();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        Set<? extends Element> classElements = roundEnvironment.getElementsAnnotatedWith(ViewOptHost.class);

        for (Element element : classElements) {
            TypeElement classElement = (TypeElement) element;
            ViewCreatorClassGenerator viewCreatorClassGenerator = new ViewCreatorClassGenerator(processingEnv, classElement, mMessager);
            viewCreatorClassGenerator.getJavaClassFile();
            break;
        }
        return true;

    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(ViewOptHost.class.getCanonicalName());
        returntypes; }}Copy the code

Core method is the process, directly to the ViewCreatorClassGenerator to generate Java classes.

Before we look at our logic, in fact, our proxy class is very simple, we just build our class name, method name, method inside, according to the View name list to write swicth.

Look at the code:

Define class name:

public ViewCreatorClassGenerator(ProcessingEnvironment processingEnv, TypeElement classElement, Messager messager) {
        mProcessingEnv = processingEnv;
        mMessager = messager;
        mTypeElement = classElement;
        PackageElement packageElement = processingEnv.getElementUtils().getPackageOf(classElement);
        String packageName = packageElement.getQualifiedName().toString();
        //classname
        String className = ClassValidator.getClassName(classElement, packageName);

        mPackageName = packageName;
        mClassName = className + "__ViewCreator__Proxy";
    }
Copy the code

Our class name is the name of the class with the annotation concatenated __ViewCreator__Proxy.

Generate class body structure:

public void getJavaClassFile() {

    Writer writer = null;
    try {
        JavaFileObject jfo = mProcessingEnv.getFiler().createSourceFile(
                mClassName,
                mTypeElement);

        String classPath = jfo.toUri().getPath();

        String buildDirStr = "/app/build/";
        String buildDirFullPath = classPath.substring(0, classPath.indexOf(buildDirStr) + buildDirStr.length());
        File customViewFile = new File(buildDirFullPath + "tmp_custom_views/custom_view_final.txt");

        HashSet<String> customViewClassNameSet = new HashSet<>();
        putClassListData(customViewClassNameSet, customViewFile);

        String generateClassInfoStr = generateClassInfoStr(customViewClassNameSet);

        writer = jfo.openWriter();
        writer.write(generateClassInfoStr);
        writer.flush();

        mMessager.printMessage(Diagnostic.Kind.NOTE, "generate file path : " + classPath);

    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if(writer ! = null) { try { writer.close(); } catch (IOException e) { // ignore } } } }Copy the code

Here we first read the tmp_custom_views/custom_view_final.txt we just generated into a hashSet.

It then passes to the generateClassInfoStr method:

private String generateClassInfoStr(HashSet<String> customViewClassNameSet) {

    StringBuilder builder = new StringBuilder();
    builder.append("// Generated code. Do not modify! \n");
    builder.append("package ").append(mPackageName).append("; \n\n");
    builder.append("import com.zhy.demo.viewopt.*; \n");
    builder.append("import android.content.Context; \n");
    builder.append("import android.util.AttributeSet; \n");
    builder.append("import android.view.View; \n");


    builder.append('\n');

    builder.append("public class ").append(mClassName).append(" implements " + sProxyInterfaceName);
    builder.append(" {\n");

    generateMethodStr(builder, customViewClassNameSet);
    builder.append('\n');

    builder.append("}\n");
    return builder.toString();

}
Copy the code

You can see that this is really just the body structure of the class stitched together.

Detailed method generation logic:

private void generateMethodStr(StringBuilder builder, HashSet<String> customViewClassNameSet) {

    builder.append("@Override\n ");
    builder.append("public View createView(String name, Context context, AttributeSet attrs ) {\n");


    builder.append("switch(name)");
    builder.append("{\n"); // switch start

    for (String className : customViewClassNameSet) {
        if (className == null || className.trim().length() == 0) {
            continue;
        }
        builder.append("case \"" + className + "\" :\n");
        builder.append("return new " + className + "(context,attrs); \n");
    }

    builder.append("}\n"); //switch end

    builder.append("return null; \n");
    builder.append(" }\n"); // method end

}
Copy the code

A for loop will do the trick.

Let’s run now:

The proxy class is generated in the following directory of the project:

Class content:

// Generated code. Do not modify!
package com.zhy.demo.viewopt;

import com.zhy.demo.viewopt.*;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;

public class ViewOpt__ViewCreator__Proxy implements IViewCreator {
    @Override
    public View createView(String name, Context context, AttributeSet attrs) {
        switch (name) {
            case "androidx.appcompat.widget.FitWindowsLinearLayout":
                return new androidx.appcompat.widget.FitWindowsLinearLayout(context, attrs);
            case "androidx.appcompat.widget.AlertDialogLayout":
                return new androidx.appcompat.widget.AlertDialogLayout(context, attrs);
            case "androidx.core.widget.NestedScrollView":
                return new androidx.core.widget.NestedScrollView(context, attrs);
            case "android.widget.Space":
                return new android.widget.Space(context, attrs);
            case "androidx.appcompat.widget.DialogTitle":
                return new androidx.appcompat.widget.DialogTitle(context, attrs);
            case "androidx.appcompat.widget.ButtonBarLayout":
                return new androidx.appcompat.widget.ButtonBarLayout(context, attrs);
            case "androidx.appcompat.widget.ActionMenuView":
                return new androidx.appcompat.widget.ActionMenuView(context, attrs);
            case "androidx.appcompat.view.menu.ExpandedMenuView":
                return new androidx.appcompat.view.menu.ExpandedMenuView(context, attrs);
            case "Button":
                return new Button(context, attrs);
            case "androidx.appcompat.widget.ActionBarContainer":
                return new androidx.appcompat.widget.ActionBarContainer(context, attrs);
            case "TextView":
                return new TextView(context, attrs);
            case "ImageView":
                return new ImageView(context, attrs);
            case "Space":
                return new Space(context, attrs);
            case "androidx.appcompat.widget.FitWindowsFrameLayout":
                return new androidx.appcompat.widget.FitWindowsFrameLayout(context, attrs);
            case "androidx.appcompat.widget.ContentFrameLayout":
                return new androidx.appcompat.widget.ContentFrameLayout(context, attrs);
            case "CheckedTextView":
                return new CheckedTextView(context, attrs);
            case "DateTimeView":
                return new DateTimeView(context, attrs);
            case "androidx.appcompat.widget.ActionBarOverlayLayout":
                return new androidx.appcompat.widget.ActionBarOverlayLayout(context, attrs);
            case "androidx.appcompat.view.menu.ListMenuItemView":
                return new androidx.appcompat.view.menu.ListMenuItemView(context, attrs);
            case "androidx.appcompat.widget.ViewStubCompat":
                return new androidx.appcompat.widget.ViewStubCompat(context, attrs);
            case "RadioButton":
                return new RadioButton(context, attrs);
            case "com.example.testviewopt.view.MyMainView4":
                return new com.example.testviewopt.view.MyMainView4(context, attrs);
            case "com.example.testviewopt.view.MyMainView3":
                return new com.example.testviewopt.view.MyMainView3(context, attrs);
            case "View":
                return new View(context, attrs);
            case "com.example.testviewopt.view.MyMainView2":
                return new com.example.testviewopt.view.MyMainView2(context, attrs);
            case "androidx.appcompat.widget.ActionBarContextView":
                return new androidx.appcompat.widget.ActionBarContextView(context, attrs);
            case "com.example.testviewopt.view.MyMainView1":
                return new com.example.testviewopt.view.MyMainView1(context, attrs);
            case "ViewStub":
                return new ViewStub(context, attrs);
            case "ScrollView":
                return new ScrollView(context, attrs);
            case "Chronometer":
                return new Chronometer(context, attrs);
            case "androidx.constraintlayout.widget.ConstraintLayout":
                return new androidx.constraintlayout.widget.ConstraintLayout(context, attrs);
            case "CheckBox":
                return new CheckBox(context, attrs);
            case "androidx.appcompat.view.menu.ActionMenuItemView":
                return new androidx.appcompat.view.menu.ActionMenuItemView(context, attrs);
            case "FrameLayout":
                return new FrameLayout(context, attrs);
            case "RelativeLayout":
                return new RelativeLayout(context, attrs);
            case "androidx.appcompat.widget.Toolbar":
                return new androidx.appcompat.widget.Toolbar(context, attrs);
            case "LinearLayout":
                return new LinearLayout(context, attrs);
        }
        returnnull; }}Copy the code

It looks perfect…

However, it is an error state at present. What error is reported?

Error: symbol not foundreturnnew Button(context,attrs); ^ Symbol: class Button position: class ViewOpt__ViewCreator__ProxyCopy the code

We notice that these system controls have no package guide.

For example, Button would be: Android.widget.button.

So we have a choice

import android.widget.*

Copy the code

One problem, however, is that not all Android views are under Android. widget. For example, a View is under Android. View and a WebView is under Android. webKit.

So we’re going to import all three packages.

This time, you will have a doubt, through the XML system can only get TextView, he zha know is android. The widget. The LinearLayout or android. The LinearLayout?

Is it difficult to try to reflect one by one?

PhoneLayoutInflater: PhoneLayoutInflater: PhoneLayoutInflater

public class PhoneLayoutInflater extends LayoutInflater {
    private static final String[] sClassPrefixList = {
        "android.widget."."android.webkit."."android.app."
    };

 
    public PhoneLayoutInflater(Context context) {
        super(context);
    }

    protected PhoneLayoutInflater(LayoutInflater original, Context newContext) {
        super(original, newContext);
    }


    @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
        for (String prefix : sClassPrefixList) {
            try {
                View view = createView(name, prefix, attrs);
                if(view ! = null) {return view;
                }
            } catch (ClassNotFoundException e) {
                // In this case we want to let the base class take a crack
                // at it.
            }
        }

        return super.onCreateView(name, attrs);
    }

    public LayoutInflater cloneInContext(Context newContext) {
        returnnew PhoneLayoutInflater(this, newContext); }}Copy the code

Loop concatenation prefix traversal…

Android.view. The prefix is in super.onCreateView:

#LayoutInflater
protected View onCreateView(String name, AttributeSet attrs)
        throws ClassNotFoundException {
    return createView(name, "android.view.", attrs);
}
Copy the code

You may not be able to find a hide View in your system because you don’t have a hide View class in your android.jar file.

Ok, here’s our proxy class:

ViewOpt__ViewCreator__Proxy

Copy the code

Was generated.

3. Write the code to generate the View

@ViewOptHost
public class ViewOpt {

    private static volatile IViewCreator sIViewCreator;

    static {
        try {
            String ifsName = ViewOpt.class.getName();
            String proxyClassName = String.format("%s__ViewCreator__Proxy", ifsName);
            Class proxyClass = Class.forName(proxyClassName);
            Object proxyInstance = proxyClass.newInstance();
            if (proxyInstance instanceof IViewCreator) {
                sIViewCreator = (IViewCreator) proxyInstance;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    public static View createView(String name, Context context, AttributeSet attrs) {


        try {
            if(sIViewCreator ! = null) { View view = sIViewCreator.createView(name, context, attrs);if(view ! = null) { Log.d("lmj", name + "Intercept build");
                }
                return view;
            }
        } catch (Throwable ex) {
            ex.printStackTrace();
        }

        returnnull; }}Copy the code

It’s basically reflecting the generated proxy object and getting an instance of it.

Then force the IViewCreator object so that we can call sIViewCreator. CreateView directly.

Do you see a point here:

Is that why apt always generates a proxy class that inherits a class or implements every interface?

This eliminates the need for reflection when calling code later.

Now that you have the logic to generate the View and inject it into the mPrivaryFactory, which is our Activity, find the BaseActivity in your project:

public class BaseActivity extends AppCompatActivity {

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        View view = ViewOpt.createView(name, context, attrs);
        if(view ! = null) {return view;
        }
        returnsuper.onCreateView(parent, name, context, attrs); }}Copy the code

The process is complete.

Run it to see log:

The 2020-05-31 18:07:26. 300, 31454-31454 /? D/ LMJ: LinearLayout interception generated 2020-05-31 18:07:26.300 31454-31454/? D/ LMJ: ViewStub intercepts generate 2020-05-31 18:07:26.300 31454-31454/? D/ LMJ: FrameLayout intercept generated 2020-05-31 18:07:26.305 31454-31454/? D/LMJ: androidx appcompat. Widget. ActionBarOverlayLayout intercept generated 2020-05-31 18:07:26. 306, 31454-31454 /? D/LMJ: androidx appcompat. Widget. ContentFrameLayout intercept generated 2020-05-31 18:07:26. 311, 31454-31454 /? D/LMJ: androidx appcompat. Widget. ActionBarContainer intercept generated 2020-05-31 18:07:26. 318, 31454-31454 /? D/LMJ: androidx appcompat. Widget. The Toolbar to intercept generated 2020-05-31 18:07:26. 321, 31454-31454 /? D/LMJ: androidx appcompat. Widget. ActionBarContextView intercept generated 2020-05-31 18:07:26. 347, 31454-31454 /? D/LMJ: androidx constraintlayout. Widget. Constraintlayout intercept generatedCopy the code

Corresponding layout:

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
Copy the code

Is it weird…

Where do you get the LinearLayout,ViewStub?

In the corresponding layout file of our Activity’s decorView.

Why is there no TextView?

Because TextView is intercepted by the support library, AppcompatTextView is generated, which is also new, so there is no need to use reflection logic.

Ok, preliminary completion.

5. A potential problem

After gradle, APT and understanding of LayoutInflater processes, we pieced relevant knowledge together to complete the layout optimization.

Isn’t that a bit of a sense of accomplishment.

However, if you are particularly familiar with APT, you should find a potential problem.

What’s the problem?

We now create two new Library modules, so that:

app implementation lib1
lib1 implementation lib2
Copy the code

Write a custom control in lib2.

We define a control Lib2View in lib2 and reference it in lib2 layout.

Lib2 XML: < androidx. Constraintlayout. Widget. Constraintlayout XMLNS: android ="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".Lib2Activity">

    <com.example.lib2.Lib2View
        android:layout_width="match_parent"
        android:layout_height="match_parent"></com.example.lib2.Lib2View>

</androidx.constraintlayout.widget.ConstraintLayout>
Copy the code

Then we execute app:assembleDebug again:

You will find that the error is reported:

Viewopt__viewcreator__proxy.java :47: Error: symbol not foundreturnnew com.example.lib2.Lib2View(context,attrs); ^ Symbol: class Lib2View location: package com.example.lib2Copy the code

The reason for the error is that although we have collected Lib2View and generated the related methods, the code cannot access the class.

Why not?

Because we’re using implementation, let’s see again:

app implementation lib1
lib1 implementation lib2
Copy the code

Implementation isolates app class references to lib2, and while packaged, everyone can access them, they are not accessible at compile time.

This is the problem with APT as I said. Apt is mainly for a single module, which is not suitable for multiple modules.

So, if you use Transfrom for related class generation, you don’t have a similar problem.

But, uh, this is my blog, and you want me to change plans?

If we change to API, or compile before, it will pass.

So can we change the implementation dynamic to an API when we package?

After exploring the Gradle API, we found that it supports:

project.afterEvaluate {
    android.libraryVariants.all { variant ->
        def variantName = variant.name
        def depSet = new HashSet()
        tasks.all {
            if ("assemble${variantName.capitalize()}".equalsIgnoreCase(it.name)) {
                project.configurations.each { conf ->
                    if (conf.name == "implementation") {
                        conf.dependencies.each { dep ->
                            depSet.add(dep)
                            project.dependencies.add("api", dep)
                        }
                        depSet.each {
                            conf.dependencies.remove(it)
                        }
                    }
                }
            }
        }
    }
}
Copy the code

We can add implementation to our API.

Apply the above script to your library build.gradle.

There are still some questions left

We demonstrated it as assembleDebug, but what about release packaging?

The release only needs to be modified in one place, the merger. XML, which is no longer under mergeDebugResources but under mergeReleaseResources.

Second, because your proxy class needs reflection, keep the relevant classes in mind.

I won’t help you deal with this. If you can’t handle release, I suggest you don’t implement this plan and study relevant knowledge points in the article first.

6. Google is doing something similar

It seems that Google is aware of the time-consuming layout build.

Cs.android.com/android/pla…

Google is working on the View Compiler, but it is not currently enabled. The runtime source code should be:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    if (DEBUG) {
        Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
              + Integer.toHexString(resource) + ")");
    }

    View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
    if(view ! = null) {return view;
    }
    XmlResourceParser parser = res.getLayout(resource);
    try {
        returninflate(parser, root, attachToRoot); } finally { parser.close(); }}Copy the code

You can see that the tryInflatePrecompiled method is added to the inflate, which looks like you can just give a layout ID and return a built View.

Look forward to the follow-up of the program online.

7. To summarize

Recently pushed a lot of performance optimization copywriting, we also ridicule are not actual combat, feel like theory.

So I took a moment to share a View to build this piece of optimization for you, you can see that there are only a few features, actually we are involved in:

  1. Gradle build knowledge;
  2. Apt related knowledge;
  3. LayooutFactory knowledge;

Although we encountered some setbacks, such as classes generated by APT at the end of the article that could not access classes in non-transition-dependent modules, we managed to solve them.

Sometimes WHEN I have problems, I comfort myself:

I feel like I can learn something again

Nothing goes right all the time. You can only grow by constantly encountering problems and solving them.

In addition, we suggest that the daily accumulation of knowledge points, do not see the article, found that they are not familiar with do not want to see, see their own already clear, see with relish…

Demo address: github.com/hongyangAnd…