鸿洋大神新作:“手把手”的性能优化文章来了

鸿洋大神新作:“手把手”的性能优化文章来了(1) UI这块知识是现今使用者最多的。当年火爆一时的Android入门培训,学会这小块知识就能随便找到不错的工作了。不过很显然现在远远不够了,拒绝无休止的CV,亲自去项目实战,读源码,研究原理吧! 阅读详情

作者:鸿洋

原文链接:https://mp.weixin.qq.com/s/ceXsH06fUFa7y4lzi4uXzw

概述

在Android开发过程中,我们基本每天都在写各种各样的xml布局文件,然后app会在运行时,将我们的布局文件转化成View显示在界面上。

这个转化,主要就是解析xml布局文件,然后根据xml的中每个View标签,将:

  1. 标签名-> View的名称
  2. 各种属性 -> AttributeSet对象

然后反射调用View两个参数的构造方法。

这也是为什么,我们在自定义控件的时候,如果需要在xml使用,需要复写其两参的构造函数。

这个设计确实极具扩展性,但是也引入了一定的性能问题。

可以很明显的看到xml文件到View这个过程中,涉及到一些耗时操作:

  1. io 操作,xml解析;
  2. 反射;

尤其是真实项目中,一些页面布局元素非常多,那么整个页面几十个控件可能都需要去反射生成。

所以很多时候,一些核心页面,为了提升构建速度,我们会考虑直接用代码生成,来替代xml写法,这样做带来一个最大的问题就是可维护性急剧下降。

在既想要可维护性又想要运行时效率的情况下,很多开发者想到,xml毕竟是非常有规律的文件,我们可以在编译时解析成View,运行时直接拿到View,就能避免IO操作以及反射操作了。

确实,想法非常完美,github上也有一个由掌阅发布的开源库:

https://github.com/iReaderAndroid/X2C

x2c的想法非常好,基本上彻底解决了我上面提出的两个耗时问题,但是引入了新的问题,就是兼容性和稳定性。

而且x2c生成代码使用了apt,apt一个都是针对本module去做一些事情,涉及到复杂的module间依赖,就会遇到很多问题,x2c在apt这方面应该也做了很多处理,但是这些处理在遇到很多项目在编译期做各种编译优化的时候,就会摩擦出一些火花。

本文也会涉及到apt,因为不涉及资源,也遇到一些问题,下文会说。

当然如果能够引入x2c,并可自维护的情况下,其实是挺好的,我非常支持这个方案,就是有一定风险。

注:本文不讨论到底哪个方案牛逼,博客更多的还是为了学习,重点还是吸收每个方案包含的知识点,扩充自己的可用知识库。

退一步

刚才我们说了,完全托管xml->View这一过程具有一定的风险,那么我们是否可退一步来看这个问题呢?

既然xml文件到View这个过程中,涉及到两个耗时点:

  • io 操作,xml解析;
  • 反射;

xml解析我们不太好干涉,这种看起来风险就高的东西还是交给Google自己吧,而且底层还涉及到有一些xmlblock的缓存逻辑。

那只剩下一个反射操作了,这是个软柿子吗?

我们有办法去除发射逻辑吗?

当然有,大家肯定都再熟悉不过了。

如果关注本号,我们在16年就写过:

探究 LayoutInflater setFactory

通过setFactory,我们不仅能够控制View的生成,甚至可以把一个View变成另一个View,比如文中,我们把TextView变成了Button。

后续换肤、黑白化一些方案都基于此。

也就说我们现在可以:

运行时,接管某个View的生成,即针对单个View标签我们可以去除反射的逻辑了。

类似代码:

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

但是,一般线上的项目都非常大,可能有各种各样的自定义View,类似上面的if else,怎么写呢?

先收集起来,然后手写?

怎么收集项目中用到的所有的View的呢?

假设我们收集到了,手写的话,项目一般都是增量的,后续新增的View怎么办呢?

可以看到我们面临两个问题:

  1. 如何收集项目中在xml中使用到的View;
  2. 如何保证写出的View生成代码,能够兼容项目的正常迭代;

确定方案

到这里目标已经确定了。

在 xml -> View的过程中,去除反射相关逻辑

来说说我们面临的两个问题如何解决:

1. 如何收集项目中在xml中使用到的View;

收集所有在xml中用到的View,有个简单的想法,我们可以解析项目中所有的layout.xml文件,不过项目中layout.xml文件每个模块都有,而且有些依赖的aar,还需要解压太难了。

细想一下,我们apk在生成过程中,资源应该需要merger吧,是不是解析某个Task merger后的产物即可。

确实有,后面详细实施会提到。

下面看第二个问题:

2. 如何保证写出的View生成代码,能够兼容项目的正常迭代;

我们已经能够收集到所使用的,所有的View列表了,那么针对这种:

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

有规律又简单的逻辑,完全可以在编译时生成一个代码类,完成相关转化代码生成,这里选择了apt。

有了xml -> View转化逻辑的代码类,最后只要在运行时,利用LayoutFactory注入即可。

3. 找一个稳妥的注入逻辑

大家都知道我们的View生成相关逻辑在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;

}

View经过mFactory2,mFactory,mPrivateFactory,如果还不能完成构建,后面等它的就是反射了。

而前两个factory,support包一般扩展功能会用,例如 TextView-> AppCompatTextView。

我们考虑利用mPrivateFactory,利用mPrivateFactory的好处就是,在目前的版本中mPrivateFactory就是Activity,所以我们只要复写Activivity的onCreateView即可:

这样完全不需要hook,也不干涉appcompat相关生成逻辑,可谓是0风险了。

对标阿里P7的性能优化学习之路

给大家免费分享的资料包括Android进阶知识体系+Android高级工程师学习手册+进阶Android阿里P7学习视频+各个大厂Android高频面试题

  • Android进阶知识体系(性能优化模块)

  • Android高级工程师学习手册(成体系)

  • 进阶Android阿里P7学习视频

评论区评论 领取资料 或者 点击 了解更多 即可领取上面所有Android高级工程师学习资料!

其他需要,也可以在我的 Github 上查看

上面所有学习内容,我愿意全部免费分享给大家。希望大家能多多支持。

开始实施

1. 获取项目中使用的控件名列表

我新建了一个项目,随便写了一些自定义控件叫MyMainView1,MyMainView,MyMainView3,MyMainView4都在layout文件中声明了,就不贴布局文件了。

之前我们说了,我们要在apk的构建过程中去寻找合适的注入点完成这个事情。

那么apk构建过程中,什么时候会merge资源呢?

我们打印下构建过程中所有的task,输入命令:

./gradlew  app:assembleDebug --console=plain

输出:

>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-DATE

哪个最像呢?一眼看有个叫:mergeDebugResources的Task,就它了。

与build目录对应,也有个mergeDebugResources的目录:

image

注意里面有个merger.xml,其中就包含了整个项目所有资源合并后的内容。

我们打开看一眼:

重点关注里面的type=layout的相关标签。

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

可以看到包含了我们layout文件的路劲,那么我们只要解析这个merger.xml,然后找到里面所有type=layout的标签,再解析出layout文件的实际路劲,再解析对应的layout xml就能拿到控件名了。

对了,这个任务要注入到mergeDebugResources后面执行。

怎么注入一个任务呢?

非常简单:

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

}

根目录:view_opt.gradle

我们首先找到mergeDebugResources这个task,再其之后,注入一个ResParseTask的任务。

然后在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)
            }
        })
    }

}

根目录:view_opt.gradle

代码很简单,主要就是解析merger.xml,找到所有的layout文件,然后解析xml,最后输出到build目录中。

代码我们都写在view_opt.gradle,位于项目的根目录,在app的build.gradle中apply即可:

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

然后我们再次运行assembleDebug,输出:

注意,上面我们还有个ignoreViewNameList对象,我们过滤了一些特殊标签,例如:“include”, “fragment”, “merge”, “view”,你可以根据输出结果自行添加。

输出结果为:

可以看到是去重后的View的名称。

这里提一下,有很多同学看到写gradle脚本就感觉恐惧,其实很简单,你就当写Java就行了,不熟悉的语法就用Java写就好了,没什么特殊的。

到这里我们就有了所有使用到的View的名称。

apt 生成代理类

有了所有用到的View的名称,接下来我们利用apt生成一个代理类,以及代理方法。

要用到apt,那么我们需要新建3个模块:

  1. ViewOptAnnotation: 存放注解;
  2. ViewOptProcessor:放注解处理器相关代码;
  3. ViewOptApi:放相关使用API的。

关于Apt的相关基础知识就不提了哈,这块知识太杂了,大家自己查阅下,后面我把demo传到github大家自己看。

我们就直接看我们最核心的Processor类了:

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

}

核心方法就是process了,直接交给了ViewCreatorClassGenerator去生成我们的Java类了。

看之前我们思考下我们的逻辑,其实我们这个代理类非常简单,我们只要构建好我们的类名,方法名,方法内部,根据View名称的列表去写swicth就可以了。

看代码吧。

定义类名:

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

我们类名就是使用注解的类名后拼接__ViewCreator__Proxy。

生成类主体结构:

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

这里首先我们读取了,我们刚才生成的tmp_custom_views/custom_view_final.txt,存放到了一个hashSet中。

然后交给了generateClassInfoStr方法:

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

}

可以看到这里其实就是拼接了类的主体结构。

详细的方法生成逻辑:

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

}

一个for循环就搞定了。

我们现在运行下。

会在项目的如下目录生成代理类:

类内容:

// 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);
        }
        return null;
    }

}

看起来很完美…

不过目前是报错状态,报什么错呢?

错误: 找不到符号
return new Button(context,attrs);
           ^
  符号:   类 Button
  位置: 类 ViewOpt__ViewCreator__Proxy

这些系统控件没有导包。

比如Button,应该是:android.widget.Button。

那么我们可以选择

import android.widget.*

不过有个问题,你会发现,android的View并不是都在android.widget下,例如View在android.view下,WebView在android.webkit下面。

所以我们要把这三个包都导入。

这个时候,你会不会有疑问,系统也只能通过xml拿到TextView,他咋知道是android.widget.LinearLayout还是android.view.LinearLayout?

难不成一个个尝试反射?

是的,你没猜错,LayoutInflater运行时的对象为: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) {
        return new PhoneLayoutInflater(this, newContext);
    }
}

循环拼接前缀遍历…

不过怎么没看到android.view.这个前缀,嗯,在super.onCreateView里面:

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

ok,这个时候,你可能还会遇到一些系统hide View找不到的情况,主要是因为你本地的android.jar里面没有那些hide View对应的class,所以编译不过,这种极少数,你可以选择在刚才过滤的List里面添加一下。

好了,到这里我们的代理类:

ViewOpt__ViewCreator__Proxy

生成了。

3. 编写生成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 + " 拦截生成");
                }
                return view;
            }
        } catch (Throwable ex) {
            ex.printStackTrace();
        }

        return null;
    }

}

其实就是反射我们刚才的生成的代理类对象,拿到它的实例。

然后强转为IViewCreator对象,这样我们后续直接 sIViewCreator.createView 调用就可以了。

这里大家有没有看到一个知识点:

就是为什么apt生成的代理类,总会让它去继承某个类或者实现某个接口?

这样在后续调用代码的时候就不需要反射了。

有了生成View的逻辑,然后注入到mPrivaryFactory就可以了,其实就是我们的Activity,找到你项目中的BaseActivity:

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;
        }
        return super.onCreateView(parent, name, context, attrs);
    }
}

流程结束。

运行下,可以看下log:

2020-05-31 18:07:26.300 31454-31454/? D/lmj: LinearLayout 拦截生成
2020-05-31 18:07:26.300 31454-31454/? D/lmj: ViewStub 拦截生成
2020-05-31 18:07:26.300 31454-31454/? D/lmj: FrameLayout 拦截生成
2020-05-31 18:07:26.305 31454-31454/? D/lmj: androidx.appcompat.widget.ActionBarOverlayLayout 拦截生成
2020-05-31 18:07:26.306 31454-31454/? D/lmj: androidx.appcompat.widget.ContentFrameLayout 拦截生成
2020-05-31 18:07:26.311 31454-31454/? D/lmj: androidx.appcompat.widget.ActionBarContainer 拦截生成
2020-05-31 18:07:26.318 31454-31454/? D/lmj: androidx.appcompat.widget.Toolbar 拦截生成
2020-05-31 18:07:26.321 31454-31454/? D/lmj: androidx.appcompat.widget.ActionBarContextView 拦截生成
2020-05-31 18:07:26.347 31454-31454/? D/lmj: androidx.constraintlayout.widget.ConstraintLayout 拦截生成

对应的布局:

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

有没有很奇怪…

哪来的LinearLayout,ViewStub这些?

其实就是我们Activity的decorView对应的布局文件里面的。

为啥没有TextView?

因为TextView并support库拦截了,生成了AppcompatTextView,也是new的,早不需要走反射逻辑了。

ok,初步完工。

点个赞再走呗!大家要多多支持鸿洋大神的文章哦!!

评论区评论 领取资料 或者 点击 了解更多 即可领取上面所有Android高级工程师学习资料!

JavaScript性能优化实战技术文章 关键要点回顾:强调DOM操作、异步处理和工具使用的重要性。持续优化建议定期审计性能。在开发早期集成优化习惯。关注社区新工具和技术(如React Concurrent Mode)。结语:鼓励读者动手实验,将优化融入日常工作流。这个大纲提供了一个逻辑框架,可以扩展为一篇完整的技术文章。每个部分都包含理论解释和可运行的代码示例,确保“实战”导向。如果需要更详细的代码或特定场景的扩展,欢迎进一步讨论! 阅读详情

相关推荐

光伏发电系统模拟及其发电预测开源python工具pvlib

本文介绍光伏发电系统模拟及其发电预测开源python工具pvlib,以及举例介绍获取辐照度方法、光伏发电预测思路。

肖永威的专栏 1万+

鸿洋大婶:自定义ViewGroup------流式标签布局

说来惭愧,我跟着视屏敲了一遍代码,受益匪浅,本想跟大家分享,奈何道行太浅,这里就不献丑解说了(偶也说不清楚),姑且做一个实用主义者吧,有需要的朋友直接复制代码就可以上手了。如果你是一只好奇的猫,建议到慕课网中倾听鸿洋大神的教诲,地址:http://www.imooc.com/video/5145直奔主题:奉上流式标签布局FlowLayout大餐。/** * Created by JACK on 1

星光小站 1594

Nordic nRF52832实战:手把手教你搭建智能手环BLE通信框架(附完整代码)

本文详细介绍了如何使用Nordic nRF52832芯片搭建智能手环的BLE通信框架,涵盖芯片优势、协议栈初始化、低功耗广播策略及健康数据服务实现。通过完整代码示例和架构设计,帮助开发者快速掌握蓝牙低功耗通信技术,适用于可穿戴设备开发。

weixin_29192141的博客 246

android okhttp工具类,基于鸿洋okhttp封装工具类okhttputils 返回数据回调封装-Go语言中文社区...

简介okhttputils 是csdn 鸿洋大神基于okhttp网络请求进行的二次封装基本请求格式如下OkHttpUtils.get().url(url).addParams("username", "hyman").addParams("password", "123").build().execute(callback);工具类中提供了 集中callback回调 可以直接获取转换后的数据类型比...

weixin_39915668的博客 657

深入了解torch框架

大国的游戏,政府支持到位,是未来;AI : Artificial Intelligence,旨在研究、开发能够模拟、延伸和扩展人类智能的理论、方法、技术及应用系统,是一种拥有自主学习和推理能力的技术。它模仿人类大脑某些功能,包括感知、学习、理解、决策和问题解决。AI本质​ NLP(说话,听)、CV(眼睛)、自动驾驶、机器人(肢体动作)、大模型三要素:数据、网络、算力① 神经网络:找到合适的数学公式;② 训练:用已有数据训练网络,目标是求最优解;③ 推理:用模型预测新样本;PyTorch,简称Torch

qq_74732076的博客 1135

张鸿洋 博客

张鸿洋 优快云认证专家,2014博客之星.鸿洋老师精通Java企业级开发,对基于Java的Web框架有一定的认知。而且还在csdn上撰写了”Android 精彩案例”一系列博文。 http://blog.csdn.net/lmj623565791   https://github.com/hongyangAndroid...

weixin_33763244的博客 2274

STM32F429网络通信避坑指南:用CubeMX搞定LAN8720+LWIP+FreeRTOS(附完整代码)

本文详细介绍了STM32F429通过CubeMX配置LAN8720+LWIP+FreeRTOS实现网络通信的避坑指南,涵盖硬件连接、CubeMX配置、LWIP优化及调试技巧,提供完整代码和实战经验,帮助开发者高效解决以太网通信中的常见问题。

weixin_26873711的博客 145

Android “退一步”的布局加载优化

注意里面有个merger.xml,其中就包含了整个项目所有资源合并后的内容。 我们打开看一眼: 重点关注里面的type=layout的相关标签。 <file name=“activity_main1” path=“/Users/zhanghongyang/work/TestViewOpt/app/src/main/res/layout/activity_main1.xml” qualifiers=“” type=“layout” /> 可以看到包含了我们layout文件的路劲,那么我们只要解析这

Java364102的博客 620

Pascal Context数据集实战:从安装到格式转换的完整指南

本文提供了Pascal Context数据集的完整实战指南,从安装配置到格式转换的详细步骤。Pascal Context是计算机视觉领域重要的语义分割数据集,包含540个精细标注类别。文章涵盖数据集下载、环境配置、预处理技巧和常见问题解决方案,帮助开发者高效利用这一数据集进行场景理解任务。

weixin_30216561的博客 616

众灵科技24路舵机控制器二次开发实战(1)-STM32串口指令控制详解

本文详细介绍了如何通过STM32单片机对众灵科技24路舵机控制器进行二次开发,实现串口指令控制。文章从硬件连接、通信协议解析到STM32代码实战,手把手指导开发者打通控制链路,并重点讲解了如何调用预设动作组驱动六足机器人,为后续集成传感器、无线遥控等高级功能奠定基础。

weixin_29188997的博客 483

鸿洋大神新作:“手把手,android实战开发视频

由于本文罗列的知识点是根据我自身总结出来的,并且由于本人水平有限,无法全部提及,欢迎大神们能补充~将来我会对上面的知识点一个一个深入学习,也希望有童鞋跟我一起学习,一起进阶。提升架构认知不是一蹴而就的,它离不开刻意学习和思考。**这里,笔者分享一份从架构哲学的层面来剖析的视频及资料分享给大家,**梳理了多年的架构经验,筹备近1个月最新录制的,相信这份视频能给你带来不一样的启发、收获。点击这里获取免费架构视频资料,第一时间获取最新知识点。

m0_60452141的博客 967

yolov5深度剖析+源码debug级讲解系列(一) yolov5的架构和源码debug准备

前言 前段时间分析了yolov3的源码,这次想带着小伙伴一起把yolov5完全掌握。博主还是保持着一贯的风格,依旧采用debug级别的源码剖析。目的就是让大家可以通过一个系列的文章就把yolov5的架构、设计理念和每一行源码都弄懂。只要小伙伴可以花时间把这个系列读完,就会对yolov5的理解有所提升。 去年参加了kaggle的小麦检测比赛,yolov5在比赛的中段开始呈现霸榜趋势,超越了efficientdet。虽然最后yolov5因为权限等某些问题被kaggle官方禁用了,但是仍然体现出了其强大的能力。值

人唯优的博客 4万+

鸿洋大神新作:“手把手”的性能优化文章来了(2)

先收集起来,然后手写?

WJud11的博客 779

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

android鸿洋布局,Android基础ConstrainLayout约束布局的介绍和使用

写在前面:之前稍微复杂的设计实现,我们都可能会借助于嵌套实现,我们知道嵌套越多,性能就越低.而我们布局一般都是在xml里面进行实现,拖拽的话估计现在android开发者都不会去使用.为了提升开发者的可视化编程,谷歌官方在2016年的i/o大会上提出了一个新的组件Constrainlayout中文可以翻译成"约束布局".听我ios的同事说他们很早之前就有了类似的组件,说实话真的是很羡慕ios开发者,...

weixin_33000813的博客 840

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

鸿洋大神2016年所有文章链接

1自定义View    Android Path酷炫动画 , 极速get花式Path [Android]五行代码实现 炫动滑动 卡片层叠布局 Android 如何实现一个平滑过渡的ViewPager广告条 Android自定义View--自己撸一个柱状图也没那么难 这交互炸了:一分钟让你拥有微信拖拽透明返回PhotoView

xiaoyangsavvy的专栏 753

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

学习资源 网站

http://blog.csdn.net/lmj623565791?viewmode=contents    --鸿洋的blog  http://blog.csdn.net/abidepan/article/details/10100485   -- fragment学习

ClearCode 419

性能优化实战(一):“性能优化”的思考

随着计算机技术的飞速发展,软件系统的复杂性和规模也在不断增长。为了确保系统能够高效、稳定地运行,性能优化成为了软件开发过程中的关键环节。尤其大型电商平台,每年应对数次大促,对技术优化有着非常大的诉求。本文将深入探讨几种常见的性能优化策略,后续文章会结合实际案例进行分析。

架构师吕师傅 1723

Typecho博客新文章自动添加“New“标签的实现方案

本文介绍了在Typecho博客系统中为7天内发布的文章标题自动添加"New"标签的实现方法。通过分析需求,提出了模板修改、插件开发和Widget扩展三种方案,最终选择Widget扩展方式。文章详细讲解了基础版和增强版代码实现,支持自定义天数、标签样式和排除特定分类等功能。同时提供了多种CSS样式设计方案和性能优化建议,包括缓存机制和数据库查询优化。最后还介绍了如何将功能集成到主题中并添加配置选项,使功能更灵活易用。该方案保持Typecho轻量特性,同时满足标识新文章的需求。

独立开发者阿乐 — AI 搜索优化(GEO)· 全栈开发 · 站点增长实战 1102

优快云如何写出”爆款“文章

总之,要在 优快云 写出爆款文章需要从选题、标题、内容、排版到发布推广等各个环节都用心经营,持续输出高质量且有吸引力的内容,才有可能打造出受广大读者喜爱的爆款文章。

2301_78398877的博客 1505

技术文章大纲:C++性能优化擂台

强调性能优化需要结合具体场景,避免过度优化。提供进一步学习的资源(书籍、工具、社区等)。

zqmgx13291的博客 492

Linux性能优化-系列文章-汇总

Linux性能优化,涉及了CPU,内存,磁盘,网络等很多方面,一方面涉及的知识面广,同时又要在原理方面掌握一定的深度。所以整理总结了Linux性能优化的一系列文章。当处理Linux性能问题的时候,可以更游刃有余。

cuiwin的专栏 946

当 AI 开始给代码做「体检报告」:初级开发者的性能优化求生欲

「工具越先进,人的核心能力就越向『理解需求』和『创造价值』靠拢」。性能优化的终极目标,从来不是写出运行最快的代码,而是写出最能满足业务需求的代码。

宝码香车的博客 890

【辉光】性能优化之道:深夜的线上告警,看算法如何让系统“起死回生”

通过一个高度仿真的“热销商品排行榜”线上性能事故,带领读者亲历一次从`O(N log N)`到`O(N log K)`,再到`O(N)`的惊心动魄的优化之旅。

光辉家族小仓库 1175

鸿洋,郭霖:2020学会这几样,Android未来属于你!最新Android技术架构演进与未来

引言 众所周知,Android是谷歌开发的一款基于Linux的开源操作系统,每年迭代一次大版本升级。 小米、华为、OPPO、VIVO、三星等各大厂商对Android原生系统进行二次开发衍生出具有各家特色的系统(比如MIUI),为手机、电视、平板电脑、手表等数十亿设备提供平台支持,使得Android作为全球最受欢迎的移动操作系统。Android诞生至今已有10余年,这一路走来Android遇到哪些问题?大版本升级朝着什么方向演进?Android的未来如何? 1. 发展历程 先来看看Android系统的发展过程

Android725的博客 2055

cefsharp 启动白屏处理_手把手讲解性能优化案例(1)启动速度优化

前言手把手讲解系列文章,是我写给各位看官,也是写给我自己的。文章可能过分详细,但是这是为了帮助到尽量多的人,毕竟工作5,6年,不能老吸血,也到了回馈开源的时候. 这个系列的文章:1、用通俗易懂的讲解方式,讲解一门技术的实用价值 2、详细书写源码的追踪,源码截图,绘制类的结构图,尽量详细地解释原理的探索过程 3、提供Github 的 可运行的Demo工程,但是我所提供代码,更多是提供思路,...

weixin_32223625的博客 1122
上一篇: 「Android高级工程师」BAT大厂面试基础题集合-下 Github标星6.5K
下一篇: Android开发12年高级工程师:作为 IT 行业的过来人,我想对你们说~(职场必看!!!)
Android725
博客等级 码龄7年 1708粉丝 350原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值