美文网首页
手撸动态换肤框架(一)

手撸动态换肤框架(一)

作者: A邱凌 | 来源:发表于2020-06-09 00:11 被阅读0次

前言

在使用网易云和QQ的过程中 发现他们的动态换肤做的很好 基本都是动态下发+不需要重启
我就参考学习了一下动态换肤框架 然后手撸一个动态换肤框架

了解

  • 首先我们需要了解一下 Activity是怎么给View设置背景 设置文字颜色的
  • 然后我们需要找到一些可以Hook的点 实现系统hook 动态换肤
  • 源码链接哈哈哈哈哈哈不想看我巴拉巴拉的同学可以直接看一下源码参考哦

Activity加载布局

篇幅有限 主要分析一下AppCompatActivity, Activity等类基本都差不多 可以自己阅读一下

  • onCreate

我们可以看到 在onCreate方法中 会将事件代理给AppCompatDelegate类 主要就是delegate.installViewFactory方法了

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        delegate.installViewFactory();
        delegate.onCreate(savedInstanceState);
        super.onCreate(savedInstanceState);
    }
    
  • installViewFactory

看一下installViewFactory实现,
我们发现如果layoutInflater.getFactory()为空的话,
会设置一个Factory2,AppCompatActivity也是集成Factory2,
创建View的事件将由Factory2实现

而且我们可以看到 需要在super.onCreate()事件之前调用 否则就会报错

    @Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
        } else {
            if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
                Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat's");
            }
        }
    }
  • 看一下Factory2
 public interface Factory2 extends Factory {
        /**
         * Version of {@link #onCreateView(String, Context, AttributeSet)}
         * that also supplies the parent that the view created view will be
         * placed in.
         *
         * @param parent The parent that the created view will be placed
         * in; <em>note that this may be null</em>.
         * @param name Tag name to be inflated.
         * @param context The context the view is being created in.
         * @param attrs Inflation attributes as specified in XML file.
         *
         * @return View Newly created view. Return null for the default
         *         behavior.
         */
        @Nullable
        View onCreateView(@Nullable View parent, @NonNull String name,
                @NonNull Context context, @NonNull AttributeSet attrs);
    }
    
}

 public interface Factory {
        /**
         * Hook you can supply that is called when inflating from a LayoutInflater.
         * You can use this to customize the tag names available in your XML
         * layout files.
         *
         * <p>
         * Note that it is good practice to prefix these custom names with your
         * package (i.e., com.coolcompany.apps) to avoid conflicts with system
         * names.
         *
         * @param name Tag name to be inflated.
         * @param context The context the view is being created in.
         * @param attrs Inflation attributes as specified in XML file.
         *
         * @return View Newly created view. Return null for the default
         *         behavior.
         */
        @Nullable
        View onCreateView(@NonNull String name, @NonNull Context context,
                @NonNull AttributeSet attrs);
    }

我们可以看到 Factory2继承自Factory 而且Factory注释也告诉我们 可以Hook这里 来实现

  • onCreateView()方法
    final View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs, boolean inheritContext,
            boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        final Context originalContext = context;

        // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
        // by using the parent's context
        if (inheritContext && parent != null) {
            context = parent.getContext();
        }
        if (readAndroidTheme || readAppTheme) {
            // We then apply the theme on the context, if specified
            context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
        }
        if (wrapContext) {
            context = TintContextWrapper.wrap(context);
        }

        View view = null;

        // We need to 'inject' our tint aware Views in place of the standard framework versions
        switch (name) {
            case "TextView":
                view = createTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ImageView":
                view = createImageView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "Button":
                view = createButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "EditText":
                view = createEditText(context, attrs);
                verifyNotNull(view, name);
                break;
            case "Spinner":
                view = createSpinner(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ImageButton":
                view = createImageButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "CheckBox":
                view = createCheckBox(context, attrs);
                verifyNotNull(view, name);
                break;
            case "RadioButton":
                view = createRadioButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "CheckedTextView":
                view = createCheckedTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "AutoCompleteTextView":
                view = createAutoCompleteTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "MultiAutoCompleteTextView":
                view = createMultiAutoCompleteTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "RatingBar":
                view = createRatingBar(context, attrs);
                verifyNotNull(view, name);
                break;
            case "SeekBar":
                view = createSeekBar(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ToggleButton":
                view = createToggleButton(context, attrs);
                verifyNotNull(view, name);
                break;
            default:
                // The fallback that allows extending class to take over view inflation
                // for other tags. Note that we don't check that the result is not-null.
                // That allows the custom inflater path to fall back on the default one
                // later in this method.
                view = createView(context, name, attrs);
        }

        if (view == null && originalContext != context) {
            // If the original context does not equal our themed context, then we need to manually
            // inflate it using the name so that android:theme takes effect.
            view = createViewFromTag(context, name, attrs);
        }

        if (view != null) {
            // If we have created a view, check its android:onClick
            checkOnClickListener(view, attrs);
        }

        return view;
    }

会一直调用到这个方法 我们发现AppCompatActivty会将所有TextView包装成兼容的AppCompatTextView

但是Activity肯定没有办法知道所有的View 所有核心方法就在createViewFromTag了

  • createViewFromTag()

createViewFromTag会调用createViewByPrefix方法,这个方法会判断一些是否是系统类,如果不是系统类 就会通过反射的方式来实现

这也是因为反射 所有效率会有所下降 有一些库就是在编译期将xml转换成具体的实际类 而不通过反射来提高效率 据说Google也在优化反射这一块

 private View createViewByPrefix(Context context, String name, String prefix)
            throws ClassNotFoundException, InflateException {
        Constructor<? extends View> constructor = sConstructorMap.get(name);

        try {
            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                Class<? extends View> clazz = Class.forName(
                        prefix != null ? (prefix + name) : name,
                        false,
                        context.getClassLoader()).asSubclass(View.class);

                constructor = clazz.getConstructor(sConstructorSignature);
                sConstructorMap.put(name, constructor);
            }
            constructor.setAccessible(true);
            return constructor.newInstance(mConstructorArgs);
        } catch (Exception e) {
            // We do not want to catch these, lets return null and let the actual LayoutInflater
            // try
            return null;
        }
    }

  • setContentView

setContentView 和上面的步骤相差不大   主要是经过XmlPullParser解析之后   调用createViewFromTag方法   这个方法我们在上面已经分析过   通过反射生成View

获取资源

我们之前获取drawable等资源 都是通过Resource.getDrawable方法来获取 ,
看一下具体实现

  • 结构

我们之前都是使用的Resouce对象  Resource是对外暴露的接口  实际更内一层是ResourceImpl   具体的实现是AssetManager

  • getDrawable()
 public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)
            throws NotFoundException {
        return getDrawableForDensity(id, 0, theme);
    }
    
 @Nullable
 public Drawable getDrawableForDensity(@DrawableRes int id, int density, @Nullable Theme theme) {
        final TypedValue value = obtainTempTypedValue();
        try {
            final ResourcesImpl impl = mResourcesImpl;
            impl.getValueForDensity(id, density, value, true);
            return impl.loadDrawable(this, value, id, density, theme);
        } finally {
            releaseTempTypedValue(value);
        }
    }

可以看到主要是调用了impl.loadDrawable方法

  • getDrawableForDensity->loadDrawableForCookie

loadDrawableForCookie 方法中会判断是否是xml   如果是xml就交给XmlResourceParser   否则交给AssetManager

AssetManager会调用native方法来创建drawable

 final InputStream is = mAssets.openNonAsset(
                            value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                    AssetInputStream ais = (AssetInputStream) is;
                    dr = decodeImageDrawable(ais, wrapper, value);

看到一个很好的图解

图解

结尾

我们上面分析了Activity onCreate 和 setContentView的过程   我们知道了Activity是如何将xml转换成View   都交由LayoutInflater代理 知道了我们应该Hook的点   我们也了解获取Resource的过程
可以知道最后的资源其实还是由AssetManager来获取   那么我们有两种实现方法了   一种是反射修改assetManager  一种是包装Resource类 具体请看手撸动态换肤框架(二)

相关文章

  • 手撸动态换肤框架(一)

    前言 了解 首先我们需要了解一下 Activity是怎么给View设置背景 设置文字颜色的 然后我们需要找到一些可...

  • 手撸动态换肤框架(二)

    我们已经学习了Activity setContentView 和Resource加载的过程 没看过可以先阅读一...

  • 动态换肤框架

    换肤模式 内置换肤 在APK包中存在多种资源(图片、颜色值等)用于换肤切换自由度低,APK文件大一般用于没有其他需...

  • 动态换肤一(前期预备知识)

      动态换肤框架是仿照网易云音乐来换肤的,换肤的方式就是通过解压 apk 文件从中获取到皮肤包的资源,然后替换我们...

  • 移动架构04-动态换肤框架

    移动架构04-动态换肤框架 一、前言 换肤就是修改app的样式(包括文字、颜色、背景等),通常用来提升用户体验。 ...

  • Android动态换肤框架-实现换肤

    1、换肤流程 2、采集流程 3、Android资源查找流程 4、采集需要换肤的控件 换肤我们需要换所有可能需要换的...

  • Android动态换肤框架-换肤原理

    注:下文源码有删减,截图只体现主流程 1、换肤原理 换肤就是替换资源(文字、颜色、图片等),而换肤基本有两种模式:...

  • LSN10-动态化换肤框架

    LSN10-动态化换肤框架 fragment源码分析 androidx.fragment.app.Fragment...

  • Android-Skin-Loader源码解析

    源码 一:简介 Android-Skin-Loader是一个通过动态加载技术实现换肤的框架;解决换肤的两个核心问题...

  • 动态换肤框架1-基础换肤

    一、换肤的两种方式 内置换肤(静态):在Apk包中存在多种资源(图片、颜色值)用于换肤时候切换。缺点是自由度低,a...

网友评论

      本文标题:手撸动态换肤框架(一)

      本文链接:https://www.haomeiwen.com/subject/sqjjtktx.html