Flutter外掛SharedPreferences原始碼分析

ershixiong發表於2019-09-03

Android上最基本的一個儲存方式就是SharedPreferences,flutter上也有一個基於sp的外掛。
外掛地址
該外掛封裝了NSUserDefaults(IOS)和SharedPreferences(Android),由於資料是非同步儲存到磁碟,不能保證在你return之後就生效, 所以儘量不要使用這個外掛儲存一些關鍵性資料。

既然是要分析原始碼,首先先把基本用法奉上。

基本用法

在專案的pubspec.yaml檔案中,新增以下內容:

dependencies:
  shared_preferences: ^0.5.3+4
複製程式碼

然後執行 packages get。接下來新建一個dart檔案,貼入如下程式碼:

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() {
  runApp(MaterialApp(
    home: Scaffold(
      body: Center(
      child: RaisedButton(
        onPressed: _incrementCounter,
        child: Text('Increment Counter'),
        ),
      ),
    ),
  ));
}

_incrementCounter() async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  int counter = (prefs.getInt('counter') ?? 0) + 1;
  print('Pressed $counter times.');
  await prefs.setInt('counter', counter);
}
複製程式碼

執行,點選螢幕中心按鈕,會看到如下列印:

I/flutter (30837): Pressed 1 times.
I/flutter (30837): Pressed 2 times.
...
複製程式碼

原始碼分析

好了,以上就是sp的用法了,是不是很簡單? ^_^
接下來一起看下原始碼,首先是獲取sp的例項:
SharedPreferences prefs = await SharedPreferences.getInstance();//注意,await必須在async修飾的函式中使用,表示非同步
對應的原始碼如下:

const MethodChannel _kChannel =
    MethodChannel('plugins.flutter.io/shared_preferences');

class SharedPreferences {
  //快取data,會跟SharedPreferences或者NSUserDefaults通過setter方法保持資料同步
  final Map<String, Object> _preferenceCache;

  //_表示建構函式私有,這裡是dart的實現單例的一種寫法
  SharedPreferences._(this._preferenceCache);

  static const String _prefix = 'flutter.';
  static SharedPreferences _instance;

  //單例獲取sp
  static Future<SharedPreferences> getInstance() async {
    if (_instance == null) {
      final Map<String, Object> preferencesMap =
          await _getSharedPreferencesMap();//這裡非同步獲取map
      _instance = SharedPreferences._(preferencesMap);
    }
    return _instance;
  }

  static Future<Map<String, Object>> _getSharedPreferencesMap() async {
    final Map<String, Object> fromSystem =
        await _kChannel.invokeMapMethod<String, Object>('getAll');//這裡對應的是native的方法呼叫,對應的實現類是SharedPreferencesPlugin。
    assert(fromSystem != null);
    // Strip the flutter. prefix from the returned preferences.
    final Map<String, Object> preferencesMap = <String, Object>{};
    for (String key in fromSystem.keys) {
      assert(key.startsWith(_prefix));
      preferencesMap[key.substring(_prefix.length)] = fromSystem[key];
    }
    return preferencesMap;
  }

  ...
}
複製程式碼

MethodChannel可用作native和flutter進行溝通,詳細可看我的另一篇文章。
傳送門
上面註釋中對應的SharedPreferencesPlugin部門原始碼如下:

//MethodCallHandler 是 MethodChannel收到flutter端呼叫時回撥的介面
public class SharedPreferencesPlugin implements MethodCallHandler {
	private static final String SHARED_PREFERENCES_NAME = "FlutterSharedPreferences";
	private final android.content.SharedPreferences preferences;

	private SharedPreferencesPlugin(Context context) {
        preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
    }

	//flutter 端呼叫都會到這裡
	public void onMethodCall(MethodCall call, MethodChannel.Result result) {
		switch (call.method) {
		    ...
			case "getAll":
	          result.success(getAllPrefs());
	          return;
	        case "setInt"://注意這裡,待會會講,先暫時略過
	          Number number = call.argument("value");
	          if (number instanceof BigInteger) {
	            BigInteger integerValue = (BigInteger) number;
	            commitAsync(
	                preferences
	                    .edit()
	                    .putString(
	                        key, BIG_INTEGER_PREFIX + integerValue.toString(Character.MAX_RADIX)),
	                result);
	          } else {
	            commitAsync(preferences.edit().putLong(key, number.longValue()), result);
	          }
	          break;
	        ...
		}
		...
	}
	//啟用asynctask來執行sp的提交邏輯並通知flutter
	private void commitAsync(final Editor editor, final MethodChannel.Result result) {
	    new AsyncTask<Void, Void, Boolean>() {
	      @Override
	      protected Boolean doInBackground(Void... voids) {
	        return editor.commit();
	      }

	      @Override
	      protected void onPostExecute(Boolean value) {
	        result.success(value);
	      }
	    }.execute();
	}

	private Map<String, Object> getAllPrefs() throws IOException {
		Map<String, ?> allPrefs = preferences.getAll();
		Map<String, Object> filteredPrefs = new HashMap<>();
		for (String key : allPrefs.keySet()) {
		if (key.startsWith("flutter.")) {//過濾出來flutter打頭的內容然後返回
			...
		}
		...
		return filteredPrefs;
	}

複製程式碼

到這裡總結一下,flutter中的sp在初始化時候,從native端拿到了所有flutter存進去的data,並傳遞給例項 Map<String, Object> _preferenceCache,最終拿到了flutter端SharedPreferences的例項。
接下來看一下如何獲取指定型別資料以及如何設定資料,獲取prefs.getInt('counter'),設定prefs.setInt('counter', counter)對應原始碼如下:

  //SharedPreferences.dart原始碼
  /// Reads a value from persistent storage, throwing an exception if it's not a
  /// bool.
  bool getBool(String key) => _preferenceCache[key];

  /// Reads a value from persistent storage, throwing an exception if it's not
  /// an int.
  int getInt(String key) => _preferenceCache[key];//直接從快取中獲取內容

  /// Reads a value from persistent storage, throwing an exception if it's not a
  /// double.
  double getDouble(String key) => _preferenceCache[key];

  /// Reads a value from persistent storage, throwing an exception if it's not a
  /// String.
  String getString(String key) => _preferenceCache[key];
  //設定資料
  Future<bool> setInt(String key, int value) => _setValue('Int', key, value);

  Future<bool> _setValue(String valueType, String key, Object value) {
    final Map<String, dynamic> params = <String, dynamic>{
      'key': '$_prefix$key',
    };//這裡的prefix其實就是static const String _prefix = 'flutter.',呼應前面看到的getAll
    if (value == null) {
      _preferenceCache.remove(key);
      return _kChannel
          .invokeMethod<bool>('remove', params)
          .then<bool>((dynamic result) => result);
    } else {
      if (value is List<String>) {
        // Make a copy of the list so that later mutations won't propagate
        _preferenceCache[key] = value.toList();
      } else {
        _preferenceCache[key] = value;
      }
      params['value'] = value;//更新map內容
      return _kChannel
          .invokeMethod<bool>('set$valueType', params)//同步到native端執行setInt方法,這裡可以回過頭來看SharedPreferencesPlugin.java的`onMethodCall`中的`setInt`方法。
          .then<bool>((dynamic result) => result);
    }
  }

複製程式碼

至此,flutter的sp原始碼已經基本分析完畢。總結一下:

  • 使用MethodChannel進行native和flutter的通訊
  • 在flutter sp初始化時候獲取了native端 name為 FlutterSharedPreferences的sp中的所有以flutter.作為字首的data資料,並快取到map
  • getXXX時直接使用map返回
  • setXXX時通過methodchannel通知native端更新sp,並更新flutter中map快取。native更新sp使用AsyncTask來實現非同步。

如果你覺得這篇文章對你有益,還請幫忙轉發和點贊,萬分感謝。

Flutter爛筆頭

相關文章