Hawk's source code parsing, a storage framework based on Shared Preferences

Posted by xenooreo on Sun, 09 Jun 2019 23:15:12 +0200

For reprinting, please mark: http://blog.csdn.net/friendlychen/article/details/76218033

I. Concept

Shared Preferences should be very familiar to you. This is a lightweight storage mechanism for simple configuration information, which stores data in the form of key-value. This article introduces a storage framework based on Shared Preferences, which is maintained by Android development God Orhan Obut open source. The well-known log framework logger came from him. It's very simple to use. Look at its introduction on github: Secure, simple key-value storage for Android. A secure and simple Android storage tool. Even the introduction is simple and straightforward. Let's look at its usage.

II. Usage

1. Adding dependencies:

compile 'com.orhanobut:hawk:2.0.1'

2. Initialization:


The above two steps are essentially necessary for the framework class. Then you can use it.
Save data:

Hawk.put(key , T );

Data acquisition:

T value= Hawk.get(key);

Delete data:


Has the data been saved:


Check the total number of stored data:


Delete all data:


Are there any simple and rude API s that are enough to meet our needs? The data T here can be arbitrary. Take a look at the official sentence: Save any type(Any objece,primitives,lists,sets,maps... ) Is it very good, because the source code has helped us do a lot of things. Next, look at what's going on inside from the source point of view. Look at a picture first. A picture is worth a thousand words.

This picture is also very clear to help us do a simple analysis. In the PUT method on the left, T value is stored in Disk. The general process is to convert the data into toString, encrypt it, serialize it, and store it in SharePreference. Getting the data is the reverse.

3. Source code parsing

First look at the initialization code:


This is the code in Hawk:

   * This will init the hawk without password protection.
   * @param context is used to instantiate context based objects.
   *                ApplicationContext will be used
  public static HawkBuilder init(Context context) {
    HawkUtils.checkNull("Context", context);
    hawkFacade = null;
    return new HawkBuilder(context);

  static void build(HawkBuilder hawkBuilder) {
    hawkFacade = new DefaultHawkFacade(hawkBuilder);

This is a simple initialization, creating the HawkBuilder object in init (), and the following is the code in HawkBuilder.

  public HawkBuilder(Context context) {
    HawkUtils.checkNull("Context", context);

    this.context = context.getApplicationContext();
    public void build() {

Here is the DefaultHawkFacade object created in build(). This creates two important class objects, HawkBuilder and Default HawkFacade, around which our operations will focus. The next step is to store the data.

  public static <T> boolean put(String key, T value) {
    return hawkFacade.put(key, value);

When we store data put, we call the put method in Default Hawk Facade.

 @Override public <T> boolean put(String key, T value) {
    // Validate
    HawkUtils.checkNull("Key", key);
    log("Hawk.put -> key: " + key + ", value: " + value);

    // If the value is null, delete it
    if (value == null) {
      log("Hawk.put -> Value is null. Any existing value will be deleted with the given key");
      return delete(key);

    // 1. Convert to text
    String plainText = converter.toString(value);
    log("Hawk.put -> Converted to " + plainText);
    if (plainText == null) {
      log("Hawk.put -> Converter failed");
      return false;

    // 2. Encrypt the text
    String cipherText = null;
    try {
      cipherText = encryption.encrypt(key, plainText);
      log("Hawk.put -> Encrypted to  " + cipherText);
    } catch (Exception e) {
    if (cipherText == null) {
      log("Hawk.put -> Encryption failed");
      return false;

    // 3. Serialize the given object along with the cipher text
    String serializedText = serializer.serialize(cipherText, value);
    log("Hawk.put -> Serialized to" + serializedText);
    if (serializedText == null) {
      log("Hawk.put -> Serialization failed");
      return false;

    // 4. Save to the storage
    if (storage.put(key, serializedText)) {
      log("Hawk.put -> Stored successfully");
      return true;
    } else {
      log("Hawk.put -> Store operation failed");
      return false;

Here is the storage core of the framework. In put (key, value) method, the key and value are judged to be null first, and the key is null, then the null pointer exception is judged in HawkUtils.checkNull () method. Value is empty, indicating that there is no data storage, then delete.

  @Override public <T> String toString(T value) {
    if (value == null) {
      return null;
    return parser.toJson(value);

The first step is to convert the data into string types using the toString method in the Converter interface. The Converter interface in Hawk gives the default concrete implementation class, HawkConverter. Here we use Gson parsing to convert data into string data.

  @Override public String encrypt(String key, String plainText) throws Exception {
    Entity entity = Entity.create(key);
    byte[] bytes = crypto.encrypt(plainText.getBytes(), entity);
    return Base64.encodeToString(bytes, Base64.NO_WRAP);

The second step is encryption. The default encryption implementation in Hawk is in ConcealEncryption, and the encryption algorithm used is a Facebook encryption algorithm. First it is a byte array, and then it is encoded by Base64 to get the string data.

@Override public <T> String serialize(String cipherText, T originalGivenValue) {
    HawkUtils.checkNullOrEmpty("Cipher text", cipherText);
    HawkUtils.checkNull("Value", originalGivenValue);

    String keyClassName = "";
    String valueClassName = "";
    char dataType;
    if (List.class.isAssignableFrom(originalGivenValue.getClass())) {
      List<?> list = (List<?>) originalGivenValue;
      if (!list.isEmpty()) {
        keyClassName = list.get(0).getClass().getName();
      dataType = DataInfo.TYPE_LIST;
    } else if (Map.class.isAssignableFrom(originalGivenValue.getClass())) {
      dataType = DataInfo.TYPE_MAP;
      Map<?, ?> map = (Map) originalGivenValue;
      if (!map.isEmpty()) {
        for (Map.Entry<?, ?> entry : map.entrySet()) {
          keyClassName = entry.getKey().getClass().getName();
          valueClassName = entry.getValue().getClass().getName();
    } else if (Set.class.isAssignableFrom(originalGivenValue.getClass())) {
      Set<?> set = (Set<?>) originalGivenValue;
      if (!set.isEmpty()) {
        Iterator<?> iterator = set.iterator();
        if (iterator.hasNext()) {
          keyClassName = iterator.next().getClass().getName();
      dataType = DataInfo.TYPE_SET;
    } else {
      dataType = DataInfo.TYPE_OBJECT;
      keyClassName = originalGivenValue.getClass().getName();

    return keyClassName + INFO_DELIMITER +
        valueClassName + INFO_DELIMITER +
        dataType + NEW_VERSION + DELIMITER +

The third step is serialization. The default implementation of serialization is HawkSerializer
Class, using reflection to get the data type of the original data. Is it a List, map, set or Object that stores different data according to different types? Returns the value of the string type, which is the type of the original key, the type of the original data, the combination of the data type and ciphertext, so that it can be stored.

  @Override public <T> boolean put(String key, T value) {
    HawkUtils.checkNull("key", key);
    return getEditor().putString(key, String.valueOf(value)).commit();

Finally, storage. The default storage implementation given by Hawk is Shared Preference Storage. As you can see from the class name, Shared Preferences is actually used to store data. I won't say much here. Shared Preferences, after all, is easy to use.
The source code for storing data put is analyzed here. To sum up, it is actually the beginning of the picture display. The data is converted into strings, encrypted, serialized and stored in four steps. Getting the data get is the reverse of the put process. There's nothing to say. As for the other methods, delete(),deleteAll(),contains(),count() are actually dealing with Shared Preferences.
Well, the source code stored in Hawk is very clear. In the analysis, we also mentioned that some implementations are default implementations already given. In fact, we can also define the interface implementations of the response according to the requirements.

  .setEncryption(new NoEncryption())
  .setLogInterceptor(new MyLogInterceptor())
  .setConverter(new MyConverter())
  .setParser(new MyParser())
  .setStorage(new MyStorage())

In fact, some API s are given in HawkBuilder for developers to define themselves. This is all set up in HawkBuilder.

public HawkBuilder setStorage(Storage storage) {
    this.cryptoStorage = storage;
    return this;

  public HawkBuilder setParser(Parser parser) {
    this.parser = parser;
    return this;

  public HawkBuilder setSerializer(Serializer serializer) {
    this.serializer = serializer;
    return this;

  public HawkBuilder setLogInterceptor(LogInterceptor logInterceptor) {
    this.logInterceptor = logInterceptor;
    return this;

  public HawkBuilder setConverter(Converter converter) {
    this.converter = converter;
    return this;

  public HawkBuilder setEncryption(Encryption encryption) {
    this.encryption = encryption;
    return this;

As Hawk introduced, it's simple and safe. Source code design is also very simple and clear. But someone might have said that. It's really very simple. I can design it, too. There must be something good about Android's design. After all, the 2.3k star on github and its value. Simple to use, clear source ideas, code profiles, in short, a good lightweight storage framework.
Written bad place, welcome to leave a message exchange.

Topics: Android github