手撸动态换肤框架(一)

前言

在使用网易云和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类 具体请看手撸动态换肤框架(二)