想写一个系列博客,关于flutter优秀插件的源码学习,flutter-plugin源码爱恨情仇,这是第一篇,本地数据持久化----shared_preference(^0.5.12+4)。

引子

刚才有个朋友问我,马老师发生什么事了,我说怎么回事,给我发了一个flutter插件,我一看,源莱氏-佐田,使用shared_preferences实现数据持久化,插件的官方描述

Wraps platform-specific persistent storage for simple data (NSUserDefaults on iOS and macOS, SharedPreferences on Android, etc.). Data may be persisted to disk asynchronously, and there is no guarantee that writes will be persisted to disk after returning, so this plugin must not be used for storing critical data.

翻译一下

为简单数据包装特定于平台的持久存储(iOS和macOS上的NSUserDefaults, Android上的SharedPreferences,等等)。数据可以异步持久化到磁盘,并且不能保证返回后的写操作将持久化到磁盘,所以这个插件不能用于存储关键数据。

源码入口

查看使用demo

Future<SharedPreferences> _prefs = SharedPreferences.getInstance();

点进去一看,啪,上来就是个单例模式,很快啊,我全部防出去了

 //构造方法私有化 
  SharedPreferences._(this._preferenceCache);

  /// Loads and parses the [SharedPreferences] for this app from disk.
  ///
  /// Because this is reading from disk, it shouldn't be awaited in
  /// performance-sensitive blocks.
  static Future<SharedPreferences> getInstance() async {
    if (_completer == null) {
      _completer = Completer<SharedPreferences>();
      try {
        final Map<String, Object> preferencesMap =
            await _getSharedPreferencesMap();
        _completer.complete(SharedPreferences._(preferencesMap));
      } on Exception catch (e) {
        // If there's an error, explicitly return the future with an error.
        // then set the completer to null so we can retry.
        _completer.completeError(e);
        final Future<SharedPreferences> sharedPrefsFuture = _completer.future;
        _completer = null;
        return sharedPrefsFuture;
      }
    }
    return _completer.future;
  }

首先是把构造方法私有化,保证其他类不能直接构造SharedPreferences的实例,只能通过getInstance这个异步方法获取实例。然后是在getInstance方法中,做一些必要的初始化操作,包括获取sp中已经存在的所有键值对到map,然后使用这个map构造SharedPreferences实例,如果发生异常,,返回一个error future的同时,会把_completer置为空,以便下次可以重试。

这一段代码里面,指向的关键代码是_getSharedPreferencesMap方法,代码如下

static Future<Map<String, Object>> _getSharedPreferencesMap() async {
    final Map<String, Object> fromSystem = await _store.getAll();
    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;
  }

这段代码做的是,获取当前平台对应的_store,用_store的getall方法,拿到已经存在的,且由flutter添加的(通过key的头判断)所有键值对数据,以map形式返回到_preferenceCache。
这样做的目的是,以后每次get数据时,可以直接从内存(_preferenceCache)拿数据,同步返回,提升效率。
set数据时,先对传入的key处理,统一加上头’flutter.‘作为标记,然后再对传入的value处理,如果value为空,从_preferenceCache和_store里删除改记录,如果value是List,复制一份值出来,然后把数据存到_preferenceCache和_store。
这里的_preferenceCache是在内存里的一个map对象,_store是SharedPreferencesStorePlatform类,是不同平台存数据的实现类。
get代码和set代码如下

//get
/// Reads a value of any type from persistent storage.
  dynamic get(String key) => _preferenceCache[key];

//set string
Future<bool> setString(String key, String value) =>_setValue('String', key, value);

Future<bool> _setValue(String valueType, String key, Object value) {
    final String prefixedKey = '$_prefix$key';
    if (value == null) {
      _preferenceCache.remove(key);
      return _store.remove(prefixedKey);
    } 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;
      }
      return _store.setValue(valueType, prefixedKey, value);
    }
  }

上文提到的获取当前平台对应的_store,代码如下

static bool _manualDartRegistrationNeeded = true;
static SharedPreferencesStorePlatform get _store {
  // This is to manually endorse the Linux implementation until automatic
  // registration of dart plugins is implemented. For details see
  // https://github.com/flutter/flutter/issues/52267.
  if (_manualDartRegistrationNeeded) {
    // Only do the initial registration if it hasn't already been overridden
    // with a non-default instance.
    if (!kIsWeb &&
        SharedPreferencesStorePlatform.instance
            is MethodChannelSharedPreferencesStore) {
      if (Platform.isLinux) {
        SharedPreferencesStorePlatform.instance = SharedPreferencesLinux();
      } else if (Platform.isWindows) {
        SharedPreferencesStorePlatform.instance = SharedPreferencesWindows();
      }
    }
    _manualDartRegistrationNeeded = false;
  }

  return SharedPreferencesStorePlatform.instance;
}

逻辑比较清晰,判断当前平台,如果是Linux平台,使用SharedPreferencesLinux,如果是windows平台,使用SharedPreferencesWindows,如果是android,ios,web(web不太确定,看代码逻辑是这样的,但是没有找到web的实现)平台,使用MethodChannelSharedPreferencesStore,然后在这个类里面再分平台调用平台的native代码

flutter调用native代码

在MethodChannelSharedPreferencesStore类里面,先设置命名通道MethodChannel,代码如下

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

然后通过_invokeBoolMethod,调用以上命名通道下的native代码,_invokeBoolMethod代码如下

Future<bool> _invokeBoolMethod(String method, Map<String, dynamic> params) {
    return _kChannel
        .invokeMethod<bool>(method, params)
        // TODO(yjbanov): I copied this from the original
        //                shared_preferences.dart implementation, but I
        //                actually do not know why it's necessary to pipe the
        //                result through an identity function.
        //
        //                Source: https://github.com/flutter/plugins/blob/3a87296a40a2624d200917d58f036baa9fb18df8/packages/shared_preferences/lib/shared_preferences.dart#L134
        .then<bool>((dynamic result) => result);
  }

android平台实现

在插件的android包下,io.flutter.plugins.sharedpreferences包中,有两个类,SharedPreferencesPlugin和MethodCallHandlerImpl,
SharedPreferencesPlugin类定义了android的命名通道方法,和之前MethodChannelSharedPreferencesStore使用相同的命名通道,然后flutter才可以找到android这边对应的方法,代码如下

private static final String CHANNEL_NAME = "plugins.flutter.io/shared_preferences";

channel = new MethodChannel(messenger, CHANNEL_NAME);

MethodCallHandlerImpl类是每一个方法在android的具体实现,可以看onMethodCall方法,实现了flutter里面调用的所有方法,代码太多不粘了。

ios平台实现

在插件的ios包下,有两个类FLTSharedPreferencesPlugin.m和FLTSharedPreferencesPlugin.h,在FLTSharedPreferencesPlugin.h
中,定义了ios的命名通道,以及每一个方法在ios的具体实现,代码如下

//命名通道
static NSString *const CHANNEL_NAME = @"plugins.flutter.io/shared_preferences";
FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:CHANNEL_NAME
binaryMessenger:registrar.messenger];

总结

插件流程,判断当前设备的平台,通过平台原生代码获取已经存在的数据,用map保存,get数据时,直接从map里面拿,set数据时,把数据加到map中,再调用平台的原生代码,存数据到磁盘。
正如官方描述的那样,这个插件做的事,就是对数据持久化这一功能,在不同平台上面的封装(wrap),整体逻辑比较清晰,代码量不大,适合我这样的菜鸡做源码阅读。其中关键的技术点,是flutter调用原生代码,这篇博客只是看了个大概流程和调用走向,对flutter调用原生代码这一块的学习,还有待具体实操。

马老师镇场