AutoX – Lazy loading of objects and image resources + Storage persistence

AutoX-lazy loading of objects and image resources + Storage persistence

1. Usage scenarios

There are many scenarios that require lazy loading. The first time I needed the lazy loading function was when writing a game automation script. I needed to detect the coordinates of an item in advance before subsequent clicks. The item’s position on the page is fixed. Yes, if I determine its coordinate point [x, y], I can always use it in the future. However, considering that the script needs to be adapted to models with different resolutions, I cannot hard-code the coordinates of this item. I can only determine it by looking for pictures and colors, and then save it to the local cache for subsequent use. Just load the cache. In the same script, there are many situations where the fixed coordinates of an item are determined first and then operated, so I need a method that can handle coordinate detection and persistence in a unified manner.

In addition, game scripts usually involve a large number of image search requirements. If you load images and release image resources separately in each method, there will be a lot of code duplication, which is not elegant enough. At this time, it is necessary to design a set of processes that can load image resources on demand, release resources uniformly, and also perform unified scaling after the images are loaded for models with different resolutions.

2. Code implementation

In response to the above two needs that I encountered in practice, the following lazy loading method was implemented by combining the API provided by AutoX and the Rhino engine features:

  • createLazyObject(obj[,saveKey]): Able to dynamically obtain and persist the property values of ordinary objects in Storage. The cache in Storage can be directly loaded at next startup; lazy loading can be re-triggered after clearing the cache. method.
  • createImageLazyObject(imagePathMap): further encapsulates the previous method, and then can trigger image loading by accessing object properties like resource.Picture 1, and then cooperate with the exit eventevents.on('exit', xxx) Completes the unified release of resources.

(There are test cases below, which can be used for direct testing)

const isFunction = (val) => typeof val === 'function';

/**
 * Create a lazy loading object. Dynamically obtained attribute values can be saved in memory or persisted to Storage, and loaded directly from Storage the next time it is run.
 * @param {object} obj
 * @param {string|null} If saveKey is empty, it will only be loaded lazily in memory; if it is not empty, it means that this object is stored in Storage with saveKey as the key.
 */
function createLazyObject(obj, saveKey) {<!-- -->
  let lazyObject = {<!-- -->};
  let cacheData = saveKey ? getCache(saveKey, {<!-- -->}) : {<!-- -->};

  for (let key in obj) {<!-- -->
    if (!obj.hasOwnProperty(key)) {<!-- -->
      continue;
    }

    // Static values are not processed
    if (!isFunction(obj[key])) {<!-- -->
      lazyObject[key] = obj[key];
      continue;
    }
    // Dynamic loading method with cached value
    if (cacheData[key] !== undefined) {<!-- -->
      lazyObject[key] = cacheData[key];
      continue;
    }
    // It is a dynamic loading method, but there is no cached value
    let tempKey = key; // Getter attribute value
    let memoryKey = '_' + key; // Actual storage attributes in memory
    let fetchMethod = obj[key];

    Object.defineProperty(lazyObject, tempKey, {<!-- -->
      get: function () {<!-- -->
        if (!this.hasOwnProperty(memoryKey)) {<!-- -->
          let value = fetchMethod();
          if (value === null || value === undefined) {<!-- -->
            console.warn(`?The result of "${<!-- -->tempKey}" lazy loading method is ${<!-- -->value}`);
          }
          Object.defineProperty(this, memoryKey, {<!-- -->
            value: value,
            writable: true,
            configurable: true,
            enumerable: false
          });
          // Save to Storage
          if (saveKey) {<!-- -->
            cacheData[tempKey] = value;
            updateCache(saveKey, cacheData);
          }
        }
        return this[memoryKey];
      },
      set: function (value) {<!-- -->
        if (value === undefined) {<!-- -->
          delete this[memoryKey];
        } else {<!-- -->
          this[memoryKey] = value;
        }
        if (saveKey) {<!-- -->
          cacheData[tempKey] = value;
          // Update the cache. Properties with undefined values will be ignored. There is a function to clear part of the cache.
          updateCache(saveKey, cacheData);
        }
      },
      enumerable: true,
      configurable: false
    });
  }

  return lazyObject;
}

const _myStorage = storages.create('xxx script cache');
/**
 * Cached data
 * @param {string} key
 * @param {any} value
 * @param {number} expire expiration timestamp, ms
 */
function updateCache(key, value, expire) {<!-- -->
  if (!key) {<!-- -->
    throw new Error('Cache key cannot be empty');
  }
  if (expire) {<!-- -->
    value.__expire = expire;
  }
  _myStorage.put(key, value);
  delete value.__expire;
}

/**
 * Read cached data. If the data does not exist or has expired, the default value or null will be returned.
 * @param {string} key key
 * @param {any} defaultValue The default value returned when the data does not exist
 * @returns If there is no cache, null will be returned.
 */
function getCache(key, defaultValue) {<!-- -->
  if (!key) {<!-- -->
    return defaultValue;
  }
  const value = _myStorage.get(key);

  if (value === undefined || (value.__expire & amp; & amp; value.__expire < Date.now())) {<!-- -->
    return defaultValue || null;
  }

  delete value.__expire;
  return value;
}

/**
 * Clear the cache, clear all caches of the current Storage when the key is empty
 * @param {string|undefined} key
 */
function clearCache(key) {<!-- -->
  if (key) {<!-- -->
    _myStorage.remove(key);
  } else {<!-- -->
    console.info('Clear all caches of the current script\\
');
    _myStorage.clear();
  }
}

// ==================== Lazy loading of images =======================
let _imageCache = {<!-- -->};
events.on('exit', function () {<!-- -->
  console.log('Release image resources', Object.values(_imageCache).length);
  Object.values(_imageCache).forEach((img) => {<!-- -->
    img.recycle();
  });
});

//Save the image path parameters through closure
function _loadImage(path) {<!-- -->
  return function () {<!-- -->
    console.log('Loading pictures:' + path);
    let img = images.read(path);
    if (img == null) {<!-- -->
      throw new Error('Image resource does not exist:' + path);
    }
    _imageCache[path] = img;
    return img;
  };
}

function createImageLazyObject(imagePathMap) {<!-- -->
  let lazyObject = {<!-- -->};
  for (let key in imagePathMap) {<!-- -->
    if (Object.hasOwnProperty.call(imagePathMap, key)) {<!-- -->
      // Convert the image path to a dynamic image loading method
      lazyObject[key] = _loadImage(imagePathMap[key]);
    }
  }

  return createLazyObject(
    lazyObject,
    undefined // It is not recommended to save image objects to Storage. If necessary, they must be converted to base64 first.
  );
}

3. Usage examples

1. Ordinary lazy loading objects

Create a lazy loading object through createLazyObject(obj[, saveKey]). Each attribute value of obj is a lazy loading method of type Function.

If the saveKey parameter is set, after the lazy loading method is triggered, the return value of the method is stored in Storage, and saveKey is used as an identifier for different lazy loading objects; if saveKey parameter is empty. After the lazy loading method is triggered, the return value is only saved in the memory. The lazy loading method will be retriggered when the attribute is used next time the program starts.

Note: Lazy loading methods should not return null or undefined

//Define two lazy loading methods
function fetchName() {<!-- -->
  console.log('Trigger name dynamic method');
  return 'John Doe'; // Assume the name obtained is 'John Doe'
}

function fetchAge() {<!-- -->
  console.log('Trigger age dynamic method');
  return 30; // Assume that the obtained age is 30
}

let identifier = 'foo';

//Define a lazy loading object
let lazyObject = createLazyObject(
  {<!-- -->
    name: fetchName,
    age: fetchAge,
    sex: 'male'
  },
  identifier // Enable Storage persistence
);

Test attribute acquisition and modification

//Clear the cache first
clearCache(identifier);
console.log(getCache(identifier));

console.log(lazyObject);

console.log('================Test acquisition===================');
console.log(lazyObject.name); // Lazy loading
console.log(lazyObject.name); // Obtained
console.log(lazyObject.age); // Lazy loading
console.log(getCache(identifier));
console.log(lazyObject);

console.log('================Test modification===================');
lazyObject.age = 20; // The lazy loading attribute is reassigned, triggering the Setter method; saved to Storage
lazyObject.sex = 'female'; // The static property is reassigned and the Setter method is not triggered; it is not saved to Storage
console.log(lazyObject.age);
console.log(getCache(identifier));
console.log(lazyObject);

console.log('================Test clear cache===================');
lazyObject.age = undefined;
console.log(lazyObject.age); // Retrigger lazy loading
console.log(getCache(identifier));
console.log(lazyObject);

2. Image resources

//Define an image lazy loading object, the value is the image path
let imgResource = createImageLazyObject({<!-- -->
  home: '/sdcard/Download/home.jpg',
  shop: '/sdcard/Download/shop.jpg',
});

console.log(imgResource);
console.log(imgResource.icon);
console.log(imgResource.icon.getWidth());
console.log(imgResource.icon2);
console.log(imgResource.icon2.getWidth());

This article continues to update the address: Lazy loading of AutoX objects and image resources + Storage persistence