Android中使用動態代理巧妙的管理SharedPreferences配置項

在Android應用程序中很多地方會使用SharedPreferences來保存配置文件,這樣你就會出現很多下面的寫法:java

// 程序配置文件
public class MyAppConfig {
    SharedPreferences mSharedPreferences;

    public MyAppConfig(Context context) {
        mSharedPreferences = context.getSharedPreferences("AppConfig", Context.MODE_PRIVATE);
    }

    /** * 設置配置1 */
    public void setConfig1(String value) {
        mSharedPreferences.edit().putString("config1", value).apply();
    }

    /** * 獲取配置1 */
    public String getConfig1() {
        return mSharedPreferences.getString("config1", "");
    }
    
    // ... 省略其餘更多的配置項
}

複製代碼

這樣一來咱們就須要寫不少跟SharedPreferences打交道的代碼,其實咱們關注的只有SET方法GET方法兩個方法罷了。那有沒有辦法,我只須要定義好配置的接口就直接獲取到值呢?這就是本篇文章要討論的啦。其實咱們理想效果應該是這樣的:git

// 定義一個配置接口
public interface ITestConfig {
    // 定義獲取配置的Get、Set方法
    void setName(String name);
    String getName();
}

// 實際操做中的調用
private void runTestMethod() {
    // 能實現這樣的效果就完美了
    ITestConfig config = XXX.create(context, ITestConfig.class);
    Log.i("rae", "結果:" + config.getName());
}


複製代碼

探索github

不妨思考一下,這種需求好像咱們在哪一個地方見過呢?沒錯,就是咱們常常用到的Retrofit框架中就有用到,我能對public <T> T create(final Class<T> service){}這個方法應該很熟悉了,咱們好奇它爲何傳入一個接口它就能構造出接口的實例呢?讓咱們走進源碼看看:json

public <T> T create(final Class<T> service) {
    Utils.validateServiceInterface(service);
    if (validateEagerly) {
      eagerlyValidateMethods(service);
    }
    
    // 關鍵的地方來了:Proxy,動態代理!
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new InvocationHandler() {
          private final Platform platform = Platform.get();
          private final Object[] emptyArgs = new Object[0];

          @Override public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args) throws Throwable {
            // If the method is a method from Object then defer to normal invocation.
            if (method.getDeclaringClass() == Object.class) {
              return method.invoke(this, args);
            }
            if (platform.isDefaultMethod(method)) {
              return platform.invokeDefaultMethod(method, service, proxy, args);
            }
            return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
          }
        });
  }

複製代碼

就是這個神奇的動態代理。具體什麼是動態代理這裏就不作解析了,有興趣的同窗能夠看看這裏介紹的Java動態代理markdown

動手前操做app

知道大概的原理以後,咱們就開始動手操做了。理一理我們的思路,定義一個動態代理實現如下功能:框架

  • 處理咱們定義的Get、Set方法,利用SharedPreferences保存配置項
  • 還能夠處理一個clear()方法清除配置
  • 還能夠處理一個remove(String key) 方法移除配置項
  • 還要處理保存對象類型(利用json字符串來實現)

動態代理的類ide

/** * 應用程序配置註解 * Created by rae on 2020-02-20. * Copyright (c) https://github.com/raedev All rights reserved. */
@Documented
@Retention(RUNTIME)
public @interface Config {

    /** * 程序配置名稱 */
    String value();

}


/** * 應用程序代理類 * Created by rae on 2020-02-20. * Copyright (c) https://github.com/raedev All rights reserved. */
public final class AppConfigHandler {

    private AppConfigHandler() {
    }

    /** * 建立程序配置代理類 * * @param cls 類的Class */
    @SuppressWarnings("unchecked")
    public static <T> T create(Context context, Class<T> cls) {
        Config config = cls.getAnnotation(Config.class);
        if (config == null) {
            throw new RuntimeException("請在配置類標註@Config()");
        }
        if (!cls.isInterface()) {
            throw new RuntimeException("配置類必須是接口");
        }
        String configName = config.value();
        if (TextUtils.isEmpty(configName)) {
            configName = cls.getName();
        }
        SharedPreferences preferences = context.getSharedPreferences(configName, Context.MODE_PRIVATE);
        // 建立動態代理
        return (T) Proxy.newProxyInstance(cls.getClassLoader(), new Class<?>[]{cls}, new ConfigProxy(preferences));
    }

    private static class ConfigProxy implements InvocationHandler {

        private final SharedPreferences mPreference;
        private final Gson mGson = new Gson();

        private ConfigProxy(SharedPreferences preference) {
            this.mPreference = preference;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) {
            String methodName = method.getName().toUpperCase();
            // 清除配置文件
            if (methodName.equalsIgnoreCase("clear")) {
                mPreference.edit().clear().apply();
            }
            // 移除配置項處理
            else if (methodName.equalsIgnoreCase("remove") && args != null) {
                String key = args[0].toString().toUpperCase();
                mPreference.edit().remove(key).apply();
            }
            // Get方法處理
            else if (methodName.startsWith("SET")) {
                setValue(methodName.replace("SET", ""), method, args);
            }
            // Set方法處理
            else if (methodName.startsWith("GET")) {
                return getValue(methodName.replace("GET", ""), method, args);
            }
            // Is方法處理,好比:isLogin()、isVip(),這類的布爾值
            else if (methodName.startsWith("IS")) {
                boolean value = mPreference.getBoolean(methodName.replace("IS", ""), false);
                return value;
            }
            return null;
        }

        /** * 設置配置值 */
        private void setValue(String name, Method method, Object[] args) {
            if (args.length != 1) throw new IllegalArgumentException("set方法的方法參數只容許一個");
            Class<?>[] parameterTypes = method.getParameterTypes();
            Class<?> parameterType = parameterTypes[0];
            Object arg = args[0];
            SharedPreferences.Editor editor = mPreference.edit();
            if (parameterType == String.class) {
                editor.putString(name, (String) arg);
            } else if (parameterType == int.class) {
                editor.putInt(name, (int) arg);
            } else if (parameterType == boolean.class) {
                editor.putBoolean(name, (boolean) arg);
            } else if (parameterType == float.class) {
                editor.putFloat(name, (float) arg);
            } else if (parameterType == long.class) {
                editor.putLong(name, (long) arg);
            } else {
                // 其餘值默認使用Json字符串
                String json = mGson.toJson(arg);
                editor.putString(name, json);
            }
            editor.apply();
        }

        /** * 獲取配置值 */
        private Object getValue(String name, Method method, Object[] args) {
            Class<?> type = method.getReturnType();
            Object defaultValue = args == null ? null : args[0];
            if (type == String.class) {
                return mPreference.getString(name, (String) defaultValue);
            } else if (type == int.class) {
                return mPreference.getInt(name, defaultValue == null ? 0 : (int) defaultValue);
            } else if (type == boolean.class) {
                return mPreference.getBoolean(name, defaultValue != null && (boolean) defaultValue);
            } else if (type == float.class) {
                return mPreference.getFloat(name, defaultValue == null ? 0 : (float) defaultValue);
            } else if (type == long.class) {
                return mPreference.getLong(name, defaultValue == null ? 0 : (long) defaultValue);
            } else {
                // 其餘值默認使用Json字符串
                String json = mPreference.getString(name, null);
                return mGson.fromJson(json, type);
            }
        }
    }
}
複製代碼

實踐操做oop

最終咱們定義一個接口,就能夠輕鬆實現讀取配置文件啦!this

/** * 程序配置 * Created by rae on 2020/2/22. * Copyright (c) https://github.com/raedev All rights reserved. */
@Config("YourAppConfig")
public interface IAppConfig {

    void setUserName(String name);

    String getUserName(String defaultValue);

    void setVip(boolean isVip);

    boolean isVip();

    void setVersion(int version);

    int getVersion();

    void clear();

    void remove(String key);
}
複製代碼

方法調用

private void runTestMethod() {
    IAppConfig config = AppConfigHandler.create(getApplicationContext(), IAppConfig.class);
    config.clear();
    config.setUserName("RAE");
    config.remove("UserName");
    Log.i("Rae", "username is " + config.getUserName("DefaultValue"));
    config.setVip(true);
    Log.i("Rae", "is vip: " + config.isVip());
    config.setVersion(10);
    Log.i("Rae", "version is " + config.getVersion());
}
複製代碼

輸出結果

I/Rae: username is DefaultValue
I/Rae: is vip: true
I/Rae: version is  10
複製代碼
相關文章
相關標籤/搜索