如何应对Android面试官->布局原理与xml解析,手写插件化换肤框架核心实现(下)

如何应对Android面试官->布局原理与xml解析,手写插件化换肤框架核心实现(上) 本节内容分为上下两章,上章主要介绍布局流程和 xml 解析流程,下章带着大家手写插件化换肤框架的核心实现; 阅读详情

前言

上一章我们讲解了布局原理与xml解析流程,以及资源加载流程、插件化换肤思路,可以查看上一章,本章咱们重点代码实现插件化换肤逻辑;

撸码实现

自定义 View 换肤控制

这里我们采用接口的方式,进行自定义 View 的换肤控制

public interface SkinCustomViewSupport {    
    void applySkin();
}

这里是一个预留接口,预留给哪些自定义的 View 且需要支持换肤的 View;

记录皮肤配置信息工具类

这里使用 MMVK、SP、DataStore、Room 等等都可以只要能用来记录当前配置的皮肤信息即可,并且在下次冷启动的时候能获取到即可,我这里使用了 SP;

public class SkinSharedPreference {    
    private static final String SKIN_SHARED = "skins";    
    private static final String KEY_SKIN_PATH = "skin-path";    
    private SharedPreferences mPref;    
    public void init(Context context) {        
        mPref = context.getSharedPreferences(SKIN_SHARED, Context.MODE_WORLD_WRITEABLE);    
    }    
    private SkinSharedPreference() {}        
    public static final class Holder {        
        private static final SkinSharedPreference instance = new SkinSharedPreference();    
    }    
    public static SkinSharedPreference getInstance() {        
        return Holder.instance;    
    }    
    /**     
      * 设置并记录当前生效的皮肤包信息     
      * @param skinPath 皮肤包路径     
      */    
    public void setSkin(String skinPath) {        
        mPref.edit().putString(KEY_SKIN_PATH, skinPath).apply();    
    }    
    /**     
      * 重置     
      */    
    public void reset() {        
        mPref.edit().remove(KEY_SKIN_PATH).apply();    
    }    
    /**     
      * 获取当前缓存的皮肤包信息     
      * @return str     
      */    
    public String getSkin() {        
        return mPref.getString(KEY_SKIN_PATH, null);    
    }
}

通过name获取皮肤中对应的资源值

换肤的本质就是将皮肤包中的同名资源值进行替换,也就是说 :在setTextColor的时候,通过传入的 R.color.white 这个white 找到皮肤包中对应的white 的值,然后传递给 setTextColor;

 通过上一章的讲解,Resources 中持有着 ResourcesImpl,ResourcesImpl 中持有着 AssetManager,我们只要拿到皮肤包的 Resources 就可以获取皮肤包中的资源值;所以我们需要一个 SkinResource 用来记录当前使用的是主 App 的 Resources 还是皮肤包中的 Resources,并通过getIdentifier getResourceEntryName getResourceTypeName来获取对应的资源值;

public class SkinResources {    
    // 原始 app 使用的 resources    
    private Resources mAppResources;    
    // 插件 apk 中的 resources    
    private Resources mSkinResources;
    // 皮肤包的包名    
    private String mSkinPkgName;
    // 当前加载的是不是默认皮肤,true是    
    private boolean isDefaultSkin = true;    
    private volatile static SkinResources instance;    
    public void init(Context context) {        
         mAppResources = context.getResources();   
    }    
    private SkinResources(Context context) {}    
    public static SkinResources getInstance() {        
        return instance;    
    }
    // 重置信息    
    public void reset() {        
        mSkinResources = null;        
        mSkinPkgName = "";        
        isDefaultSkin = true;    
    }
    // 应用传递进来的皮肤    
    public void applySkin(Resources resources, String pkgName) {        
        mSkinResources = resources;        
        mSkinPkgName = pkgName;        
        isDefaultSkin = TextUtils.isEmpty(pkgName) || resources == null;    
    }    
    /**     
      * 1. 通过主 app 中的 resId(R.color/drawable.XX) 获取到自己的名字     
      * 2. 根据名字和类型获取皮肤包中的 ID     
      *     
      * @param resId resId     
      */    
    private int getIdentifier(int resId) {        
        if (isDefaultSkin) {            
            return resId;        
        }        
        //        
        String resName = mAppResources.getResourceEntryName(resId);        
        String resType = mAppResources.getResourceTypeName(resId);        
        return mSkinResources.getIdentifier(resName, resType, mSkinPkgName);    
    }    
    /**     
      * 获取颜色值     
      *     
      * @param resId resId     
      */    
    public int getColor(int resId) {        
        if (isDefaultSkin) {            
            return mAppResources.getColor(resId);        
        }        
        int skinId = getIdentifier(resId);        
        if (skinId == 0) {            
            return mAppResources.getColor(resId);        
        }        
        return mSkinResources.getColor(resId);    
    }    
    public ColorStateList getColorStateList(int resId) {        
        if (isDefaultSkin) {            
            return mAppResources.getColorStateList(resId);        
        }        
        int skinId = getIdentifier(resId);        
        if (skinId == 0) {            
            return mAppResources.getColorStateList(resId);        
        }        
        return mSkinResources.getColorStateList(resId);    
    }    
    /**     
      * 获取 drawable     
      *     
      * @param resId resId     
      */    
    public Drawable getDrawable(int resId) {        
        if (isDefaultSkin) {            
            return mAppResources.getDrawable(resId);        
        }        
        int skinId = getIdentifier(resId);        
        if (skinId == 0) {            
            return mAppResources.getDrawable(resId);        
        }        
        return mSkinResources.getDrawable(resId);    
    }    
    /**     
      * 可能是颜色值,可能是 drawable     
      *     
      * @param resId resId     
      */    
    public Object getBackground(int resId) {       
        String resourceTypeName = mAppResources.getResourceTypeName(resId);        
        if ("color".equals(resourceTypeName)) {            
            return getColor(resId);        
        } else {            
            return getDrawable(resId);        
        }    
    }
}

状态栏的换肤支持

当我们页面进行换肤的时候,我们的状态栏也是应该要跟着整体颜色进行改变的,也就是说我们需要动态的修改对应页面状态栏的颜色,这里写一个工具类,用来切换状态栏

public class SkinThemeUtils {    
    private static final int[] APPCOMPAT_COLOR_PRIMARY_DARK_ATTRS = {android.R.attr.colorPrimaryDark};    
    private static final int[] STATUS_BAR_COLOR_ATTRS = {android.R.attr.statusBarColor, android.R.attr.navigationBarColor};    
    /**     
      * 获得 Theme 中属性中定义的资源 id     
      *     
      * @param context context     
      * @param attrs attrs     
      * @return int[]     
      * */    
    public static int[] getResId(Context context, int[] attrs) {        
        int[] resIds = new int[attrs.length];        
        TypedArray a = context.obtainStyledAttributes(attrs);        
        for (int i = 0; i < attrs.length; i++) {            
            resIds[i] = a.getResourceId(i, 0);        
        }        
        a.recycle();        
        return resIds;    
    }    
    /**     
     *  更新导航栏,状态栏的颜色值     
     *     
     * @param activity  activity 当前页面     
     *     
     * */    
    public static void updateStatusBarColor(Activity activity) {        
        // 不处理 5.0 以下的        
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {            
            return;        
        }        
        // 获得 statusBarColor 与 navigationBarColor        
        // 当与 colorPrimaryDark 不同时 以 statusBarColor 为准        
        int[] resIds = getResId(activity, STATUS_BAR_COLOR_ATTRS);        
        int statusBarColorResId = resIds[0];        
        int navigationBarColor = resIds[1];        
        // 如果直接在 style 中写入固定颜色值(而不是 @color/XXX ) 获得0,不处理,这里需要和业务侧订好规范        
        if (statusBarColorResId != 0) {            
            int color = SkinResources.getInstance().getColor(statusBarColorResId);            
            activity.getWindow().setStatusBarColor(color);        
        } else {            
            int colorPrimaryDarkResId = getResId(activity, APPCOMPAT_COLOR_PRIMARY_DARK_ATTRS)[0];            
            if (colorPrimaryDarkResId != 0) {                
                int color = SkinResources.getInstance().getColor(colorPrimaryDarkResId);                
                activity.getWindow().setStatusBarColor(color);            
            }        
        }        
        if (navigationBarColor != 0) {            
            int color = SkinResources.getInstance().getColor(navigationBarColor);            
            activity.getWindow().setNavigationBarColor(color);        
        }    
    }
}

记录所有需要换肤的View的所有属性

在创建 View 的时候,我们需要记录下这个 view 的所有可以换肤的属性,例如:

<TextView    
    android:id="@+id/storage"    
    android:layout_width="match_parent"    
    android:layout_height="50dp"    
    app:layout_constraintStart_toStartOf="parent"    
    app:layout_constraintEnd_toEndOf="parent"    
    app:layout_constraintTop_toTopOf="parent"    
    android:text="@string/scope_storage"    
    android:textColor="@color/material_on_background_disabled"    
    android:gravity="center"/>

我们要拿到 TextView  的 textColor 属性,并记录下来,以及拿到对应的 id 值 记录下来

static class SkinNameValuePair {    
    // 属性名    
    String attributeName;    
    // 对应资源 id    
    int resId;    
    SkinNameValuePair(String attributeName, int resId) {        
        this.attributeName = attributeName;        
        this.resId = resId;    
    }
}

有时候,一个View 可能会设置多个支持换肤的属性,那么就需要一个集合来封装,这里声明了一个内部类,View 和对应的 List 集合;

public static class SkinView {    
    public View view;    
    // 这个 View 换肤的属性与它对应的 id 的集合    
    List<SkinNameValuePair> skinNameValuePairs;    
    SkinView(View view, List<SkinNameValuePair> skinPairs) {        
        this.view = view;        
        this.skinPairs = skinNameValuePairs;    
    }    
    void applySkin() {        
        applySkinSupport();        
        for (SkinNameValuePair skinPair : skinPairs) {            
            Drawable left = null, top = null, right = null, bottom = null;            
            switch (skinPair.attributeName) {                
                case "background":                    
                    Object background = SkinResources.getInstance().getBackground(skinPair.resId);                    
                    if (background instanceof Integer) {                        
                        view.setBackgroundColor((Integer) background);                    
                    } else {                        
                        ViewCompat.setBackground(view, (Drawable) background);                    
                    }                    
                    break;                
                case "src":                  
                    background = SkinResources.getInstance().getBackground(skinPair.resId);                    
                    if (background instanceof Integer) {                        
                        ((ImageView) view).setImageDrawable(new ColorDrawable((Integer) background));                    
                    } else {                        
                        ((ImageView) view).setImageDrawable((Drawable) background);                    
                    }                    
                    break;                
                case "textColor":                    
                    ((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList(skinPair.resId));                    
                    break;                
                case "drawableLeft":                    
                    left = SkinResources.getInstance().getDrawable(skinPair.resId);                    
                    break;                
                case "drawableTop":                    
                    top = SkinResources.getInstance().getDrawable(skinPair.resId);                    
                    break;                
                case "drawableRight":                    
                    right = SkinResources.getInstance().getDrawable(skinPair.resId);                    
                    break;                
                case "drawableBottom":                    
                    bottom = SkinResources.getInstance().getDrawable(skinPair.resId);                    
                    break;            
            }            
            if (left != null || null != top || right != null || null != bottom) {                
                ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);            
            }        
        }    
    }    
    private void applySkinSupport() {        
        if (view instanceof SkinCustomViewSupport) {            
            ((SkinCustomViewSupport) view).applySkin();        
        }    
    }
}

接下来,就需要收集当前 View 支持换肤的属性,并将这些支持换肤的 View 的属性封装到一个集合中,整体就是下面这样的一个层级

接下来就是遍历这个 View 的所有要换肤的属性并记录下来;

class SkinAttributeAttrs {    
    private final List<SkinView> mSkinViews = new ArrayList<>();    
    private static final List<String> mAttributes = new ArrayList<>();    
    static {        
        mAttributes.add("background");        
        mAttributes.add("src");       
        mAttributes.add("textColor");        
        mAttributes.add("drawableLeft");        
        mAttributes.add("drawableTop");        
        mAttributes.add("drawableRight");        
        mAttributes.add("drawableBottom");    
    }    
    /**     
      * 寻找所有可以换肤的 view     
      * 以 ?开头的 和 以 @ 开头的属性值     
      */    
    void searchView(View view, AttributeSet attrs) {        
        List<SkinNameValuePair> skinPairs = new ArrayList<>();        
        for (int i = 0; i < attrs.getAttributeCount(); i++) {            
            // 获取属性名            
            String attributeName = attrs.getAttributeName(i);            
            if (mAttributes.contains(attributeName)) {                
                // 获取属性值                
                String attributeValue = attrs.getAttributeValue(i);                
                // 比如 color,以 # 号写死的颜色
                // 不支持换肤 android:textColor="#FFFFFFFF" 这种不知道插件皮肤对应的值               
                if (attributeValue.startsWith("#")) {                    
                    continue;                
                }                
                int resId;                
                if (attributeValue.startsWith("?")) {                    
                    // 以 ? 开头的属性值                    
                    int attrId = Integer.parseInt(attributeValue.substring(1));                    
                    resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];                
                } else {                    
                    // 正常以 @ 开头的属性值                    
                    resId = Integer.parseInt(attributeValue.substring(1));                
                }                
                SkinNameValuePair skinPair = new SkinNameValuePair(attributeName, resId);                
                skinPairs.add(skinPair);            
            }        
        }        
        if (!skinPairs.isEmpty() || view instanceof SkinCustomViewSupport) {            
            SkinView skinView = new SkinView(view, skinPairs);            
            mSkinViews.add(skinView);        
        }    
    }    
    /**     
      * 对所有的 View 中的所有属性进行皮肤修改     
      */    
    void applySkin() {        
        for (SkinView mSkinView : mSkinViews) {            
            mSkinView.applySkin();        
        }    
    }
}

接下来就是要接管 View 的创建过程了;

接管 View 的创建流程

上一章讲解过,接管 View 的创建过程,只需要替换 Factory2 接口即可,这里我们也实现 Factory2 接口

public class SkinLayoutInflaterFactory2 implements LayoutInflater.Factory2, Observer {    
    private static final String[] mClassPrefixList = {            
        "android.widget.",            
        "android.webkit.",            
        "android.app.",            
        "android.view.",    
    };    
    // 记录对应 view 的构造函数    
    private static final Class<?>[] mConstructorSignature = new Class[] {            
        Context.class, AttributeSet.class    
    };    
    private static final HashMap<String, Constructor<? extends View>> sConstructorMap = new HashMap<>();    // 当选择新皮肤后需要替换 View 与之对应的属性    
    // 页面属性管理器    
    private final SkinAttributeAttrs mSkinAttribute;    
    // 用于获取窗口的状态框的信息    
    private final Activity mActivity;    
    SkinLayoutInflaterFactory2(Activity activity) {        
        this.mActivity = activity;        
        this.mSkinAttribute = new SkinAttributeAttrs();    
    }    
    @Override    
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {        
        // 换肤就是在需要的时候替换 View 的属性(src background 等)        
        // 所以这里创建 view 从而修改 View 属性        
        View view = createSystemView(name, context, attrs);        
        if (view == null) {            
            view = createView(name, context, attrs);        
        }        
        if (null != view) {            
            // 加载属性            
            mSkinAttribute.searchView(view, attrs);        
        }        
        return view;    
    }    
    @Override    
    public View onCreateView(String name, Context context, AttributeSet attrs) {        
        View view = createSystemView(name, context, attrs);        
        if (view == null) {            
            view = createView(name, context, attrs);        
        }        
        if (null != view) { 
            // 处理换肤的逻辑           
            mSkinAttribute.searchView(view, attrs);        
        }        
        return view;
    }    
    /**     
      * @param name View的类名     
      * @param context 上下文     
      * @param attrs 属性     
      * @return View对象     
      */    
    private View createSystemView(String name, Context context, AttributeSet attrs) {        
        // 如果包含 . 则不是 SDK 中的 View 可能是自定义 View 包括 support 库中的 View        
        if (-1 != name.indexOf('.')) {            
            return null;        
        }        
        for (String s : mClassPrefixList) {
            // 找到了对应的 View 之后返回            
            View view = createView(s + name, context, attrs);            
            if (view != null) {                
                return view;            
            }        
        }        
        return null;    
    }    
    /**     
      * 反射创建全类名View对象     
      *     
      * @param name 全类名View     
      * @param context 上下文     
      * @param attrs 属性     
      * @return View对象     
      */    
    private View createView(String name, Context context, AttributeSet attrs) {        
        Constructor<? extends View> constructor = findConstructor(context, name);        
        try {            
            return constructor.newInstance(context, attrs);        
        } catch (Exception e) {            
            e.printStackTrace();        
        }        
        return null;    
    }    
    /**     
      * 根据全类名找到其对应的构造方法对象     
      *     
      * @param context 上下文     
      * @param name 全类名View     
      * @return 构造方法对象     
      */    
    private Constructor<? extends View> findConstructor(Context context, String name) {        
        Constructor<? extends View> constructor = sConstructorMap.get(name);        
        if (constructor == null) {            
            try {                
                Class<? extends View> clazz = context.getClassLoader().loadClass(name).asSubclass(View.class);                
                constructor = clazz.getConstructor(mConstructorSignature);                
                constructor.setAccessible(true);                
                sConstructorMap.put(name, constructor);            
            } catch (Exception e) {                
                e.printStackTrace();            
            }        
        }        
        return constructor;    
    }    
    /**     
      *  Activity(Observable)发出通知,这里就会执行     
      *     
      *  执行换肤操作     
      * */    
    @Override    
    public void update(Observable o, Object arg) {        
        SkinThemeUtils.updateStatusBarColor(mActivity);        
        mSkinAttribute.applySkin();    
    }
}

接管了 View 的创建流程,这里大部分都是直接抄的源码的创建逻辑,在 View 创建的时候,就可以替换成插件包中皮肤;

接下来就需要监听 Activity 的创建和销毁,在创建之后的回调了重置 mFactorySet 这里 API 也提供了相关接口;

public class SkinActivityLifeCycle extends AbsActivityLifeCycle {    
    private final Observable mObservable;    
    private final ArrayMap<Activity, SkinLayoutInflaterFactory2> mLayoutInflaterFactory = new ArrayMap<>();    
    SkinActivityLifeCycle(Observable observable) {        
        this.mObservable = observable;    
    }    
    @Override    
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {        
        /*         
         *  onCreate方法执行之后 回调到这里,此时Activity的onCreate已经执行完毕,也就是setContentView方法已经执行结束,         
         *  mFactorySet已经被设置为true,所以要反射修改为false,否则会抛出 IllegalStateException("A factory has already been set on this LayoutInflater")         
         * */        
        /*         
         * 更新状态栏         
         * */        
        SkinThemeUtils.updateStatusBarColor(activity);        
        /*         
         * 更新布局视图         
         * */        
        LayoutInflater layoutInflater = activity.getLayoutInflater();        
        try {            
            // Android布局加载器 使用 mFactorySet 标记是否设置过 Factory            
            // 如设置过,再次设置会抛出异常            
            // 设置 mFactorySet 为 false            
            Field field = LayoutInflater.class.getDeclaredField("mFactorySet");            
            field.setAccessible(true);            
            field.setBoolean(layoutInflater, false);        
        } catch (Exception e) {            
            e.printStackTrace();        
        }        
        SkinLayoutInflaterFactory2 skinLayoutInflaterFactory = new SkinLayoutInflaterFactory2(activity);        
        LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutInflaterFactory);        
        mLayoutInflaterFactory.put(activity, skinLayoutInflaterFactory);        
        // 观察者模式       
        mObservable.addObserver(skinLayoutInflaterFactory);    
    }    
    @Override    
    public void onActivityDestroyed(Activity activity) {        
        SkinLayoutInflaterFactory2 skinLayoutInflaterFactory = mLayoutInflaterFactory.remove(activity);        
        SkinManager.getInstance().deleteObserver(skinLayoutInflaterFactory);    
    }
}

加载皮肤资源,初始化配置信息,

public class SkinManager extends Observable {    
    private Application mContext;
    private Resources skinResources;    
    private SkinManager() {}    
    /**     
      * 初始化必须放在 application 中     
      * */    
    public void init(Application application) {        
        mContext = application;        
        SkinSharedPreference.getInstance().init(application);        
        SkinResources.init(application);        
        // 注册 Activity 生命周期,并设置被观察者        
        SkinActivityLifeCycle skinApplicationActivityLifeCycle = new SkinActivityLifeCycle(this);        
        application.registerActivityLifecycleCallbacks(skinApplicationActivityLifeCycle);        
        // 加载上次使用保存的皮肤        
        loadSkin(SkinSharedPreference.getInstance().getSkin());    
    } 
    public Resources getSkinResources() {
        return skinResources;
    }   
    public static SkinManager getInstance() {        
        return Holder.instance;    
    }    
    private static final class Holder {        
        private static final SkinManager instance = new SkinManager();    
    }    
    /**     
      *  加载皮肤并应用     
      *  @param skin skin 皮肤路径     
      *     
      *  对外暴露这个方法,可以添加设置页面,增加换肤按钮,点击事件中 调用此方法,传递皮肤包路径     
      *     
      * */    
    public void loadSkin(String skin) {        
        if (TextUtils.isEmpty(skin)) {            
            // 还原默认皮肤            
            SkinSharedPreference.getInstance().reset();            
            SkinResources.getInstance().reset();        
        } else {            
            // 宿主 app 的 resources            
            Resources resources = mContext.getResources();            
            // 反射创建 AssetManager 与 Resources            
            try {                
                AssetManager assetManager = AssetManager.class.newInstance();                
                // 资源路径设置目录或者压缩包                
                // TODO 同一个 key 的颜色值 就会被替换掉,所以未打开的Activity页面,在初次打开的时候,使用的是接管 View 创建的过程,同时替换成新的皮肤资源                
                // TODO Resources进行getColor(R.color.xxx) 因为是同名的,这里已经替换成皮肤插件包中的Resources了,所以未打开过的Activity,执行onCreate方法中的setContentView                
                // TODO 中的inflate的时候 读取到的颜色值 就是插件包中的                
                Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);                
                addAssetPath.setAccessible(true);                
                addAssetPath.invoke(assetManager, skin);                
                // 根据当前的设备显示器信息与配置(横竖屏,语言等) 创建 Resources,                
                skinResources = new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());                
                // 获取外部 apk(皮肤包) 包名                
                PackageManager mPm = mContext.getPackageManager();                
                PackageInfo info = mPm.getPackageArchiveInfo(skin, PackageManager.GET_ACTIVITIES);                
                if(info == null) {
                    return;
                }
                String packageName = info.packageName;                
                // 这里就会进行一个赋值,如果皮肤插件包有值,就会给SkinResources进行赋值                
                SkinResources.getInstance().applySkin(skinResources, packageName);                
                // 记录                
                SkinSharedPreference.getInstance().setSkin(skin);            
            } catch (Exception e) {                
                //            
            }        
        }        
        // 通知采集的 view 更新皮肤        
        // 被观察者改变,通知所有的观察者        
        setChanged();        
        notifyObservers(null);    
    }
}

Application 中初始化插件化换肤框架

public class MyApplication extends Application {    
    public static Application sApplication;    
    @Override    
    public void onCreate() {        
        super.onCreate();        
        sApplication = this;        
        SkinManager.getInstance().init(this);    
    }    
    public static Application getApplication() {        
        return sApplication;    
    }    
    @Override    
    public Resources getResources() {        
        Resources skinResources = SkinManager.getInstance().getSkinResources();        
        if (skinResources != null) {            
            if (skinResources.getConfiguration() != getSuperResources().getConfiguration()                     
                || skinResources.getDisplayMetrics() != getSuperResources().getDisplayMetrics()) {                
                skinResources.updateConfiguration(getSuperResources().getConfiguration(), 
                    getSuperResources().getDisplayMetrics());            
            }            
            return skinResources;        
        }        
        return getSuperResources();    
    }    
    @Override    
    public void onConfigurationChanged(@NonNull Configuration newConfig) {        
        super.onConfigurationChanged(newConfig);        
        // 适配 Android 12 屏幕切换 resources 不更新的问题        
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {            
            getSuperResources().updateConfiguration(newConfig, Resources.getSystem().getDisplayMetrics());        
        }    
    }    
    public Resources getSuperResources() {        
        return super.getResources();    
    }
}

换肤按钮切换通知

// 这里也可以是网络下载的地址

SkinManager.getInstance().loadSkin("/data/data/com.example.llc/skin/skin-debug.apk");

最终效果

PS:TODO 版本适配,以及编译版本和目标版本

mFactorySet 做了版本限制,当 targetSdkVersion > 27 的时候会抛出异常,因为涉及到了插件化Hook,demo中做了 compileSdkVersion 和 targetSdkVersion 都是 27 的限制,主要是为了讲解 xml 解析和布局原理,后面我会补上适配的内容;

根据注释提示,可以参考 AsyncLayoutInflater 的实现自己的 LayoutInflater 接管 View 的创建;

简历润色

简历上可写:深度理解布局原理和XML解析,可手写插件化换肤核心实现;

下一章预告

嵌套滑动原理,手写淘宝首页二级联动

欢迎三连

来都来了,点个关注点个赞吧,你的支持是我最大的动力;

RK3572-Android16-记录一次OTA升级失败解决方案 这里记录一次解决RK3576 Android16 OTA 用Recovery 升级失败的一次解决案例,将部分经验作为总结。本次OTA升级耗时了几天,最终是RK给出的解决方案,再加上经验来解决了其它部分问题新平台,高一点的Android版本,可能本身有bug,多请教平台来,不要蒙着干,干不出来的。 阅读详情

相关推荐

win11下的MySQL安装

目前官方最新版本是8.0.26,考虑到其稳定性、可靠性还需一定周期保证,所以使用官方版求稳定仍然建议5.7系列。3、选择MySQL Servers 5.7.38 -x64,然后按绿色箭头将选中的版本移植到右边选中框中。4、点击下方蓝色Advanced Options,选择安装位置。1、在开始界面搜索MySQL,点击command line。4、在“系统变量”中选择“path”,点击编辑。5、新建——>增加MySQL路径——>确定,12、execute,安装好后next。9、不需要做修改,直接点击next。.

rensweet的博客 2万+

如何应对Android面试官->布局原理与xml解析,手写插件化换肤框架

本节内容分为上下两章,上章主要介绍布局流程和 xml 解析流程,下章带着大家手写插件化换肤框架的核心实现;

z1ztai的博客 614

从BUCK-BOOST到反激:一个‘拆电感’的视角,轻松理解多路输出交叉调整

本文从BUCK-BOOST电路到反激变换器的演变入手,通过‘拆解电感’的独特视角解析多路输出交叉调整机制。深入探讨了反激电路的能量暂存方式、传递路径变化及电压关系,并结合具体参数对比揭示其特性。针对多路输出中的电压平衡问题,提供了绕组排布优化、元器件选型等实战技巧,帮助工程师有效提升电源系统的稳定性与效率。

weixin_33738578的博客 436

Android 14+ 模拟器抓包实录:把 Burp CA 装成系统 CA,让 targetSdk≥34 的 App 乖乖解密

本文记录了在 Android 14+ 环境下,通过将 Burp CA 安装为系统信任锚,成功抓取 targetSdk=35 App 的 HTTPS 明文流量的完整实操过程。针对“浏览器可用、App 不可用”的典型现象,精准识别出根本原因是系统级证书信任策略变更(用户安装的 CA 被屏蔽),而非 SSL Pinning。仅通过 adb 命令将 CA 推送至系统证书存储并重启生效,无需 Frida、Objection 或修改 APK,实现零侵入解密。全文命令均基于 Windows + MuMu 模拟器环境实测通

JIANqiao19931029的博客 341

无刷直流电机矢量控制(四):基于滑模观测器的无传感器控制

无刷直流电机矢量控制,采用转速、电流双环控制,电流环dq解耦,d轴采用id=0控制,q轴采用转速外环。采用滑模观测器(SMO),根据电压电流信息进行电机位置角度动态估算,模型可直接运行,转速和角度跟踪精度较高。 本文整理了该方法的基本原理,介绍了在MATLAB/Simulink中的建模和仿真过程,最后附上完整的模型文件。 一、基本原理 滑模观测器(下文简称SMO)是基于滑模变结构控制方法的一种状态观测器。其针对无刷直流电机转子位置估算的实现过程如下。 假设采用矢量控...

Forster-C的博客 6865

Android14 有线网络静态IP配置:从固件烧录到系统默认设置

本文详细解析了在Android14系统中,如何通过修改系统框架层和设置应用,为设备预设有线网络静态IP地址,实现固件烧录后即具备指定网络配置的功能。文章深入探讨了从扩展SettingsProvider数据库字段、配置默认值,到在设置应用中读取和应用配置的完整流程,旨在解决工业平板、智能终端等设备在无DHCP网络环境中批量部署的痛点,提升开箱即用体验。

lake5的博客 918

具身智能 2026:启元人形机器人开发者生态的“安卓时刻”来了吗

一个贯穿全文的问题:人形机器人开发者生态,是否正从「封闭垂直」走向「开源平台化」?本文以开源底座的公开数据为标尺,盘点 SDK、URDF、仿真与技能分发的开放程度,并落回启元机器人的开源路径。所有关键数据均标注来源,文末附社区与学堂链接。

在北风吹起的时候加入我们的队伍 在秋雨到来的时候成为彼此的温暖 743

科研党福音:如何用Zotero+Paper Agent打造你的AI论文速递系统(附详细配置教程)

本文为科研工作者提供了一套利用Zotero和开源工具Paper Agent构建自动化AI论文速递系统的详细教程。该系统能基于用户在Zotero中的文献库智能学习研究兴趣,每日自动从arXiv等平台筛选、下载并深度解读最相关的前沿论文,生成个性化日报,实现从被动接收到主动精准获取的知识工作流重塑。

kite3的博客 513

android-ad-cleaner:android免 Root 批量关闭系统开屏与营销推送

橘子,计算机视觉方向工程师,深耕 AIGC 与多媒体处理领域,专注技术实用化落地。本文为原创内容,转载请注明来源与作者。优快云 首发 | 版权所有 © 2026。

下载资源或查看教程前,请务必阅读置顶的《免责声明》 500

别再只用真彩色了!手把手教你用ENVI主成分分析(PCA)给遥感图像‘美颜’与‘瘦身’

本文详细介绍了如何利用ENVI主成分分析(PCA)技术优化遥感图像处理,实现数据‘美颜’与‘瘦身’。通过PCA的维度压缩、噪声分离和特征增强三大优势,显著提升高光谱数据的可视化效果和分析效率。文章还提供了ENVI操作的最佳实践和常见问题解决方案,帮助读者掌握这一遥感图像处理的游戏规则改变者。

weixin_28465009的博客 275

ubuntu20.04升级OpenSSH

业务需求,需要将服务器的OpenSSH版本从8.2升级到最新版本(9.9),由于apt能提供的最新版本就是8.2,所以需要采用源码安装的方式。

xiamu66的博客 1524

Android 14 SELinux策略深度解析

init 的全局公开约束(如 neverallow init execute_no_trans,放 public 才能约束所有分区的规则)这就是"哪些可见"的强制机制。alps_s0(MTK 工程)的 device/mediatek/sepolicy/basic/ 提供。所有分区(system/system_ext/product/vendor)都可引用。init的公开约束、跨分区共用的 type/attribute/neverallow。——vendor 的 .cil 只能引用 plat 的。

新时代农民工 102

PostgreSQL性能优化:如何搭建监控体系?

本文系统讲解如何构建企业级PostgreSQL监控体系,覆盖四大核心维度:基础设施层、数据库实例层、会话与查询层、对象与存储层。通过Prometheus+Grafana+ELK等工具链实现指标采集、可视化与告警闭环管理,详细解析关键监控指标、部署方法及告警策略设计,帮助DBA快速定位慢查询、连接泄漏、存储膨胀等典型故障,构建"看得见、管得住、防得早"的数据库运维能力。

数据知道的博客 2232

Android 开发入门实践(二):Kotlin 空安全,从可空类型到安全访问

延续 NewsApp 项目中的新闻标题练习,记录 Kotlin 安全调用。

Easyhood的博客 276

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

Android 模块化收口:能力组合框架、手写 LifecycleOwner 与热修复共存

二十多个模块业务高度相似却各有差异,核心矛盾在于“统一与灵活”的平衡。通过角色接口组合(如IProjectAware、IVoiceAware)实现能力按需注入,新增功能零侵入;用模板方法锁定生命周期流程,确保顺序一致;结合@CallSuper与工厂方法,既约束关键逻辑,又保留子类自由。最终实现:骨架统一、填空自由,管得住业务,又不侵入业务。

mmsx990的博客 395

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

Android Message源码阅读

本文梳理了Android Handler机制中Message的核心设计:链表结构、消息复用池(sPool)及回收机制,重点解析了异步消息与同步屏障(SyncBarrier)的协同机制。当主线程需优先处理UI绘制等高优先级任务时,系统通过postSyncBarrier插入屏障,使Looper跳过普通同步消息,优先执行标记为异步的UI渲染任务,确保帧率流畅。该机制是保障Android界面响应性的关键技术之一。

u012116089的博客 183

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

安卓部署Linux性能实测:一颗移动处理器,能否胜任日常 Linux 开发?

本文通过在一加Ace5上部署NimoteCode的ARM64 Linux环境,测试了其在CoreMark、SQLite编译及并行构建中的表现。结果显示,尽管单核性能约2.34GHz,多核扩展比达6.15×,但优化编译耗时仍高达35秒,反映移动处理器虽具备一定开发能力,却受限于文件系统开销与运行环境(如PRoot)。实际开发中,非优化构建效率较高,而并行编译可显著提速。结论:安卓手机可作为轻量级开发辅助工具,但无法替代桌面环境,真正价值在于便携性与即时响应,而非全功能替代。

一个不知名码农的专栏 283

Android中Gradle网络下载超时、更改存放位置(解决方案)

在国内开发Android应用,需要使用到一些外网才能正常访问的环境依赖。除了挂梯子,这里再提供几个常见问题的配置方案,可以已定程度减少下载超时问题。但是要注意,不同的开发环境、网络环境、时间段,出现的问题都是不可控的,遇到配置问题,建议保持耐心,多做尝试。

indeedes的博客 122

云手机私有化部署完整方案:基于ARM服务器搭建私有安卓云集群

ARM 服务器是私有化安卓云集群最优硬件底座,原生架构匹配 Android,高密度、低损耗,非常适合企业内网场景。整套私有化方案的核心,从来不是 “跑起来安卓”,而是隔离可控、可审计、可运维、可扩容。两条路线各有取舍:开源自建灵活、门槛高;商用私有化交付开箱即用、安全能力完备,是当前企业生产环境主流方案。六边云手机私有化方案,补齐了自建方案在驱动适配、管控平台、审计合规、信创适配上的短板,适合对数据主权、内网隔离有强需求的政企、自研 App、垂直行业客户。

a598652的博客 308

Android 开发入门实践(九):把新闻数据与页面状态交给 ViewModel

本文记录第110—117课学习成果:将页面标题、新闻列表及选中编号逐步迁移至NewsViewModel,通过ViewModelProvider获取实例并读取数据。页面调用ViewModel方法处理选择与返回,实现状态管理解耦。过程中纠正了对Activity重建机制的误解,并厘清mutableStateOf的可观察性与ViewModel对象生命周期的区别,深化了对Compose状态更新机制的理解。

Easyhood的博客 203

Android 17 全栈性能分析关键字与源码手册

FrameTimeline 找异常帧,ATrace 找阶段,thread_state 找等待类型,ftrace 找内核原因,Simpleperf 找 CPU 热点,驱动源码找最终触发条件;任何根因结论都必须回到同一对象、同一时间窗口和同一条依赖链。

weixin_43228946的博客 362

CTF 命令执行题学习笔记:终端特殊字符 $IFS 与 %09 的用法(HelloCTF RCE-labs)

空格被过滤时:用%09(TAB)或$IFS代替空格关键词被过滤时:用通配符?或字符类[f]拼出被禁的词,让 WAF 在原始字符串里看不到它,shell 在执行时再展开这类技巧在后续的命令执行题里会反复用到,建议亲手在靶机上敲一遍感受一下。

YY888888_的博客 538

Android 顶点上传:Heap 到 Direct 的零拷贝,省的是 JNI 次数

一帧数十万次putFloat的性能瓶颈源于频繁跨JNI边界,每次开销远超传输字节数。通过将写入与上传解耦:用堆内ByteBuffer高速写入,最后一次性put(writeBuf)批量传至堆外DirectByteBuffer,将数百万次边界调用压缩为一次,实现近10倍性能提升。关键在于复用堆外缓冲区、统一端序,并结合“更新-绘制”分离,仅在数据变更时上传顶点,平移缩放时零开销重绘,彻底释放CPU瓶颈。

mmsx990的博客 474

Android 系统启动与调试中的日志抓取方式全解析

│ 用户操作/系统事件 ││ │▼ ▼│ (驱动/硬件/内存管理) │ │ (App/SystemServer) ││ │ │ ││ │▼ ▼│ dmesg 命令 │ │ logd daemon ││ cat /proc/kmsg │ │ (分类存储到分区) ││ │▼ ▼│ 串口 UART │ │ logcat 命令 ││ (硬件直连,最早输出) │ │ adb logcat ││ │▼│ (汇总所有日志+系统状态) │工具最佳用途关键优势主要局限dmesg。

zhaoshuzhaoshu的专栏 256

go消息推送之极光推送 (JPush REST API + SDK,覆盖安卓与iOS)

使用 gorush有个绕不开的问题:国内安卓设备上 FCM 基本不可达,华为、小米、OPPO、vivo 各自为政——此时可用极光推送(JPush)作解决方案。它的思路和前面几家都不一样:App 集成极光 SDK 后拿到统一的 RegistrationID,服务端发一条 REST 请求,极光云端自动把消息路由到 APNs、各家厂商通道或极光自有长连接。这里给出了:一个用纯标准库直接调用极光的;另一个用社区维护的,类型化参数,接口注释与官方文档逐条对应。两个功能相同,可放在一起对比学习。

qq_40467670的博客 161
上一篇: 如何应对Android面试官->布局原理与xml解析,手写插件化换肤框架核心实现(上)
下一篇: 如何应对Android面试官->嵌套滚动原理大揭秘,实战京东首页二级联动
老A说
博客等级 码龄14年 1181粉丝 85原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值