亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Flutter如何保證數(shù)據(jù)操作原子性詳解

 更新時(shí)間:2022年03月04日 16:05:55   作者:水花DX  
這篇文章主要給大家介紹了關(guān)于Flutter如何保證數(shù)據(jù)操作原子性的相關(guān)資料,文章通過(guò)實(shí)例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下

前言

Flutter 是單線程架構(gòu),按道理理說(shuō),F(xiàn)lutter 不會(huì)出現(xiàn) Java 的多線程相關(guān)的問(wèn)題。

但在我使用 Flutter 過(guò)程中,卻發(fā)現(xiàn) Flutter 依然會(huì)存在數(shù)據(jù)操作原子性的問(wèn)題。

其實(shí) Flutter 中存在多線程的(Isolate 隔離池),只是 Flutter 中的多線程更像 Java 中的多進(jìn)程,因?yàn)?Flutter 中線程不能像 Java 一樣,可以兩個(gè)線程去操作同一個(gè)對(duì)象。

我們一般將計(jì)算任務(wù)放在 Flutter 單獨(dú)的線程中,例如一大段 Json 數(shù)據(jù)的解析,可以將解析計(jì)算放在單獨(dú)的線程中,然后將解析完后的 Map<String, dynamic> 返回到主線程來(lái)用。

Flutter單例模式

在 Java 中,我們一般喜歡用單例模式來(lái)理解 Java 多線程問(wèn)題。這里我們也以單例來(lái)舉例,我們先來(lái)一個(gè)正常的:

class FlutterSingleton {
  static FlutterSingleton? _instance;

  /// 將構(gòu)造方法聲明成私有的
  FlutterSingleton._();

  static FlutterSingleton getInstance() {
    if (_instance == null) {
      _instance = FlutterSingleton._();
    }
    return _instance!;
  }
}

由于 Flutter 是單線程架構(gòu)的, 所以上述代碼是沒(méi)有問(wèn)題的。

問(wèn)題示例

但是, 和 Java 不同的是, Flutter 中存在異步方法。

做 App 開(kāi)發(fā)肯定會(huì)涉及到數(shù)據(jù)持久化,Android 開(kāi)發(fā)應(yīng)該都熟悉 SharedPreferences,F(xiàn)lutter 中也存在 SharedPreferences 庫(kù),我們就以此來(lái)舉例。同樣實(shí)現(xiàn)單例模式,只是這次無(wú)可避免的需要使用 Flutter 中的異步:

class SPSingleton {
  static SPSingleton? _instance;

  String? data;

  /// 將構(gòu)造方法聲明成私有的
  SPSingleton._fromMap(Map<String, dynamic> map) : data = map['data'];

  static Future<SPSingleton> _fromSharedPreferences() async {
    // 模擬從 SharedPreferences 中讀取數(shù)據(jù), 并以此來(lái)初始化當(dāng)前對(duì)象
    Map<String, String> map = {'data': 'mockData'};
    await Future.delayed(Duration(milliseconds: 10));
    return SPSingleton._fromMap(map);
  }

  static Future<SPSingleton> getInstance() async {
    if (_instance == null) {
      _instance = await SPSingleton._fromSharedPreferences();
    }
    return _instance!;
  }
}

void main() async {
  SPSingleton.getInstance().then((value) {
    print('instance1.hashcode = ${value.hashCode}');
  });
  SPSingleton.getInstance().then((value) {
    print('instance2.hashcode = ${value.hashCode}');
  });
}

運(yùn)行上面的代碼,打印日志如下:

instance1.hashcode = 428834223
instance2.hashcode = 324692380

可以發(fā)現(xiàn),我們兩次調(diào)用 SPSingleton.getInstance() 方法,分別創(chuàng)建了兩個(gè)對(duì)象,說(shuō)明上面的單例模式實(shí)現(xiàn)有問(wèn)題。

我們來(lái)分析一下 getInstance() 方法:

static Future<SPSingleton> getInstance() async {
  if (_instance == null) { // 1
    _instance = await SPSingleton._fromSharedPreferences(); //2
  }
  return _instance!;
}

當(dāng)?shù)谝淮握{(diào)用 getInstance() 方法時(shí),代碼在運(yùn)行到 1 處時(shí),發(fā)現(xiàn) _instance 為 null, 就會(huì)進(jìn)入 if 語(yǔ)句里面執(zhí)行 2 處, 并因?yàn)?await 關(guān)鍵字掛起, 并交出代碼的執(zhí)行權(quán), 直到被 await 的 Future 執(zhí)行完畢,最后將創(chuàng)建的 SPSingleton 對(duì)象賦值給 _instance 并返回。

當(dāng)?shù)诙握{(diào)用 getInstance() 方法時(shí),代碼在運(yùn)行到 1 處時(shí),可能會(huì)發(fā)現(xiàn) _instance 還是為 null (因?yàn)?await SPSingleton._fromSharedPreferences() 需要 10ms 才能返回結(jié)果), 然后和第一次調(diào)用 getInstance() 方法類似, 創(chuàng)建新的 SPSingleton 對(duì)象賦值給 _instance。

最后導(dǎo)致兩次調(diào)用 getInstance() 方法, 分別創(chuàng)建了兩個(gè)對(duì)象。

解決辦法

問(wèn)題原因知道了,那么該怎樣解決這個(gè)問(wèn)題呢?

究其本質(zhì),就是 getInstance() 方法的執(zhí)行不具有原子性,即:在一次 getInstance() 方法執(zhí)行結(jié)束前,不能執(zhí)行下一次 getInstance() 方法。

幸運(yùn)的是, 我們可以借助 Completer 來(lái)將異步操作原子化,下面是借助 Completer 改造后的代碼:

import 'dart:async';

class SPSingleton {
  static SPSingleton? _instance;
  static Completer<bool>? _monitor;

  String? data;

  /// 將構(gòu)造方法聲明成私有的
  SPSingleton._fromMap(Map<String, dynamic> map) : data = map['data'];

  static Future<SPSingleton> _fromSharedPreferences() async {
    // 模擬從 SharedPreferences 中讀取數(shù)據(jù), 并以此來(lái)初始化當(dāng)前對(duì)象
    Map<String, String> map = {'data': 'mockData'};
    await Future.delayed(Duration(milliseconds: 10));
    return SPSingleton._fromMap(map);
  }

  static Future<SPSingleton> getInstance() async {
    if (_instance == null) {
      if (_monitor == null) {
        _monitor = Completer<bool>();
        _instance = await SPSingleton._fromSharedPreferences();
        _monitor!.complete(true);
      } else {
        // Flutter 的 Future 支持被多次 await
        await _monitor!.future;
        _monitor = null;
      }
    }
    return _instance!;
  }
}

void main() async {
  SPSingleton.getInstance().then((value) {
    print('instance1.hashcode = ${value.hashCode}');
  });
  SPSingleton.getInstance().then((value) {
    print('instance2.hashcode = ${value.hashCode}');
  });
}

我們?cè)俅畏治鲆幌?getInstance() 方法:

static Future<SPSingleton> getInstance() async {
  if (_instance == null) { // 1
    if (_monitor == null) { // 2
      _monitor = Completer<bool>(); // 3
      _instance = await SPSingleton._fromSharedPreferences(); // 4
      _monitor!.complete(true); // 5
    } else {
      // Flutter 的 Future 支持被多次 await
      await _monitor!.future; //6
      _monitor = null;
    }
  }
  return _instance!; // 7
}

當(dāng)?shù)谝淮握{(diào)用 getInstance() 方法時(shí), 1 處和 2 處都會(huì)判定為 true, 然后進(jìn)入執(zhí)行到 3 處創(chuàng)建一個(gè)的 Completer 對(duì)象, 然后在 4 的 await 處掛起, 并交出代碼的執(zhí)行權(quán), 直到被 await 的 Future 執(zhí)行完畢。

此時(shí)第二次調(diào)用的 getInstance() 方法開(kāi)始執(zhí)行,1 處同樣會(huì)判定為 true, 但是到 2 處時(shí)會(huì)判定為 false, 從而進(jìn)入到 else, 并因?yàn)?6 處的 await 掛起, 并交出代碼的執(zhí)行權(quán);

此時(shí), 第一次調(diào)用 getInstance() 時(shí)的 4 處執(zhí)行完畢, 并執(zhí)行到 5, 并通過(guò) Completer 通知第二次調(diào)用的 getInstance() 方法可以等待獲取代碼執(zhí)行權(quán)了。

最后,兩次調(diào)用 getInstance() 方法都會(huì)返回同一個(gè) SPSingleton 對(duì)象,以下是打印日志:

instance1.hashcode = 786567983
instance2.hashcode = 786567983

由于 Flutter 的 Future 是支持多次 await 的, 所以即便是連續(xù) n 次調(diào)用 getInstance() 方法, 從第 2 到 n 次調(diào)用會(huì) await 同一個(gè) Completer.future, 最后也能返回同一個(gè)對(duì)象。

Flutter任務(wù)隊(duì)列

雖然我們經(jīng)常拿單例模式來(lái)解釋說(shuō)明 Java 多線程問(wèn)題,可這并不代表著 Java 只有在單例模式時(shí)才有多線程問(wèn)題。

同樣的,也并不代表著 Flutter 只有在單例模式下才有原子操作問(wèn)題。

問(wèn)題示例

我們同樣以數(shù)據(jù)持久化來(lái)舉例,只是這次我們以數(shù)據(jù)庫(kù)操作來(lái)舉例。

我們?cè)诓僮鲾?shù)據(jù)庫(kù)時(shí),經(jīng)常會(huì)有這樣的需求:如果數(shù)據(jù)庫(kù)表中存在這條數(shù)據(jù),就更新這條數(shù)據(jù),否則就插入這條數(shù)據(jù)。

為了實(shí)現(xiàn)這樣的需求,我們可能會(huì)先從數(shù)據(jù)庫(kù)表中查詢數(shù)據(jù),查詢到了就更新,沒(méi)查詢到就插入,代碼如下:

class Item {
  int id;
  String data;
  Item({
    required this.id,
    required this.data,
  });
}

class DBTest {
  DBTest._();
  static DBTest instance = DBTest._();
  bool _existsData = false;
  Future<void> insert(String data) async {
    // 模擬數(shù)據(jù)庫(kù)插入操作,10毫秒過(guò)后,數(shù)據(jù)庫(kù)中才有數(shù)據(jù)
    await Future.delayed(Duration(milliseconds: 10));
    _existsData = true;
    print('執(zhí)行了插入');
  }

  Future<void> update(String data) async {
    // 模擬數(shù)據(jù)庫(kù)更新操作
    await Future.delayed(Duration(milliseconds: 10));
    print('執(zhí)行了更新');
  }

  Future<Item?> selected(int id) async {
    // 模擬數(shù)據(jù)庫(kù)查詢操作
    await Future.delayed(Duration(milliseconds: 10));
    if (_existsData) {
      // 數(shù)據(jù)庫(kù)中有數(shù)據(jù)才返回
      return Item(id: 1, data: 'mockData');
    } else {
      // 數(shù)據(jù)庫(kù)沒(méi)有數(shù)據(jù)時(shí),返回null
      return null;
    }
  }

  /// 先從數(shù)據(jù)庫(kù)表中查詢數(shù)據(jù),查詢到了就更新,沒(méi)查詢到就插入
  Future<void> insertOrUpdate(int id, String data) async {
    Item? item = await selected(id);
    if (item == null) {
      await insert(data);
    } else {
      await update(data);
    }
  }
}

void main() async {
  DBTest.instance.insertOrUpdate(1, 'data');
  DBTest.instance.insertOrUpdate(1, 'data');
}

我們期望的輸出日志為:

執(zhí)行了插入
執(zhí)行了更新

但不幸的是, 輸出的日志為:

執(zhí)行了插入
執(zhí)行了插入

原因也是異步方法操作數(shù)據(jù), 不是原子操作, 導(dǎo)致邏輯異常。

也許我們也可以效仿單例模式的實(shí)現(xiàn),利用 Completer 將 insertOrUpdate() 方法原子化。

但對(duì)于數(shù)據(jù)庫(kù)操作是不合適的,因?yàn)槲覀兛赡苓€有其它需求,比如說(shuō):調(diào)用插入數(shù)據(jù)的方法,然后立即從數(shù)據(jù)庫(kù)中查詢這條數(shù)據(jù),發(fā)現(xiàn)找不到。

如果強(qiáng)行使用 Completer,那么到最后,可能這個(gè)類中會(huì)出現(xiàn)一大堆的 Completer ,代碼難以維護(hù)。

解決辦法

其實(shí)我們想要的效果是,當(dāng)有異步方法在操作數(shù)據(jù)庫(kù)時(shí),別的操作數(shù)據(jù)的異步方法應(yīng)該阻塞住,也就是同一時(shí)間只能有一個(gè)方法來(lái)操作數(shù)據(jù)庫(kù)。我們其實(shí)可以使用任務(wù)隊(duì)列來(lái)實(shí)現(xiàn)數(shù)據(jù)庫(kù)操作的需求。

我這里利用 Completer 實(shí)現(xiàn)了一個(gè)任務(wù)隊(duì)列:

import 'dart:async';
import 'dart:collection';

/// TaskQueue 不支持 submit await submit, 以下代碼就存在問(wèn)題
///
/// TaskQueue taskQueue = TaskQueue();
/// Future<void> task1(String arg)async{
///   await Future.delayed(Duration(milliseconds: 100));
/// }
/// Future<void> task2(String arg)async{
///   在這里submit時(shí), 任務(wù)會(huì)被添加到隊(duì)尾, 且當(dāng)前方法任務(wù)不會(huì)結(jié)束
///   添加到隊(duì)尾的任務(wù)必須等到當(dāng)前方法任務(wù)執(zhí)行完畢后, 才能繼續(xù)執(zhí)行
///   而隊(duì)尾的任務(wù)必須等當(dāng)前任務(wù)執(zhí)行完畢后, 才能執(zhí)行
///   這就導(dǎo)致相互等待, 使任務(wù)無(wú)法進(jìn)行下去
///   解決辦法是, 移除當(dāng)前的 await, 讓當(dāng)前任務(wù)結(jié)束
///   await taskQueue.submit(task1, arg);
/// }
///
/// taskQueue.submit(task2, arg);
///
/// 總結(jié):
/// 被 submit 的方法的內(nèi)部如果調(diào)用 submit 方法, 此方法不能 await, 否則任務(wù)隊(duì)列會(huì)被阻塞住
///
/// 如何避免此操作, 可以借鑒以下思想:
/// 以數(shù)據(jù)庫(kù)操作舉例, 有個(gè)save方法的邏輯是插入或者更新(先查詢數(shù)據(jù)庫(kù)select,再進(jìn)行下一步操作);
/// sava方法內(nèi)部submit,并且select也submit, 就容易出現(xiàn)submit await submit的情況
///
/// 我們可以這樣操作,假設(shè)當(dāng)前類為 DBHelper:
/// 將數(shù)據(jù)庫(kù)的增,刪,查,改操作封裝成私有的 async 方法, 且私有方法不能使用submit
/// DBHelper的公有方法, 可以調(diào)用自己的私有 async 方法, 但不能調(diào)用自己的公有方法, 公有方法可以使用submit
/// 這樣就不會(huì)存在submit await submit的情況了
class TaskQueue {
  /// 提交任務(wù)
  Future<O> submit<A, O>(Function fun, A? arg) async {
    if (!_isEnable) {
      throw Exception('current TaskQueue is recycled.');
    }
    Completer<O> result = new Completer<O>();

    if (!_isStartLoop) {
      _isStartLoop = true;
      _startLoop();
    }

    _queue.addLast(_Runnable<A, O>(
      fun: fun,
      arg: arg,
      completer: result,
    ));
    if (!(_emptyMonitor?.isCompleted ?? true)) {
      _emptyMonitor?.complete();
    }

    return result.future;
  }

  /// 回收 TaskQueue
  void recycle() {
    _isEnable = false;
    if (!(_emptyMonitor?.isCompleted ?? true)) {
      _emptyMonitor?.complete();
    }
    _queue.clear();
  }

  Queue<_Runnable> _queue = Queue<_Runnable>();
  Completer? _emptyMonitor;
  bool _isStartLoop = false;
  bool _isEnable = true;

  Future<void> _startLoop() async {
    while (_isEnable) {
      if (_queue.isEmpty) {
        _emptyMonitor = new Completer();
        await _emptyMonitor!.future;
        _emptyMonitor = null;
      }

      if (!_isEnable) {
        // 當(dāng)前TaskQueue不可用時(shí), 跳出循環(huán)
        return;
      }

      _Runnable runnable = _queue.removeFirst();
      try {
        dynamic result = await runnable.fun(runnable.arg);
        runnable.completer.complete(result);
      } catch (e) {
        runnable.completer.completeError(e);
      }
    }
  }
}

class _Runnable<A, O> {
  final Completer<O> completer;
  final Function fun;
  final A? arg;

  _Runnable({
    required this.completer,
    required this.fun,
    this.arg,
  });
}

由于 Flutter 中的 future 不支持暫停操作, 一旦開(kāi)始執(zhí)行, 就只能等待執(zhí)行完。

所以這里的任務(wù)隊(duì)列實(shí)現(xiàn)是基于方法的延遲調(diào)用來(lái)實(shí)現(xiàn)的。

TaskQueue 的用法示例如下:

void main() async {
  Future<void> test1(String data) async {
    await Future.delayed(Duration(milliseconds: 20));
    print('執(zhí)行了test1');
  }

  Future<String> test2(Map<String, dynamic> args) async {
    await Future.delayed(Duration(milliseconds: 10));
    print('執(zhí)行了test2');
    return 'mockResult';
  }

  TaskQueue taskQueue = TaskQueue();
  taskQueue.submit(test1, '1');
  taskQueue.submit(test2, {
    'data1': 1,
    'data2': '2',
  }).then((value) {
    print('test2返回結(jié)果:${value}');
  });

  await Future.delayed(Duration(milliseconds: 200));
  taskQueue.recycle();
}
/*
執(zhí)行輸出結(jié)果如下:

執(zhí)行了test1
執(zhí)行了test2
test2返回結(jié)果:mockResult
*/

值得注意的是: 這里的 TaskQueue 不支持 submit await submit, 原因及示例代碼已在注釋中說(shuō)明,這里不再贅述。

為了避免出現(xiàn) submit await submit 的情況,我代碼注釋中也做出了建議(假設(shè)當(dāng)前類為 DBHelper):

  • 將數(shù)據(jù)庫(kù)的增、刪、查、改操作封裝成私有的異步方法, 且私有異步方法不能使用 submit;

  • DBHelper 的公有方法, 可以調(diào)用自己的私有異步方法, 但不能調(diào)用自己的公有異步方法, 公有異步方法可以使用 submit;

這樣就不會(huì)出現(xiàn) submit await submit 的情況了。

于是,上述的數(shù)據(jù)庫(kù)操作示例代碼就變成了以下的樣子:

class Item {
  int id;
  String data;
  Item({
    required this.id,
    required this.data,
  });
}

class DBTest {
  DBTest._();
  static DBTest instance = DBTest._();
  TaskQueue _taskQueue = TaskQueue();
  bool _existsData = false;
  Future<void> _insert(String data) async {
    // 模擬數(shù)據(jù)庫(kù)插入操作,10毫秒過(guò)后,數(shù)據(jù)庫(kù)才有數(shù)據(jù)
    await Future.delayed(Duration(milliseconds: 10));
    _existsData = true;
    print('執(zhí)行了插入');
  }

  Future<void> insert(String data) async {
    await _taskQueue.submit(_insert, data);
  }

  Future<void> _update(String data) async {
    // 模擬數(shù)據(jù)庫(kù)更新操作
    await Future.delayed(Duration(milliseconds: 10));
    print('執(zhí)行了更新');
  }

  Future<void> update(String data) async {
    await _taskQueue.submit(_update, data);
  }

  Future<Item?> _selected(int id) async {
    // 模擬數(shù)據(jù)庫(kù)查詢操作
    await Future.delayed(Duration(milliseconds: 10));
    if (_existsData) {
      // 數(shù)據(jù)庫(kù)中有數(shù)據(jù)才返回
      return Item(id: 1, data: 'mockData');
    } else {
      // 數(shù)據(jù)庫(kù)沒(méi)有數(shù)據(jù)時(shí),返回null
      return null;
    }
  }

  Future<Item?> selected(int id) async {
    return await _taskQueue.submit(_selected, id);
  }

  /// 先從數(shù)據(jù)庫(kù)表中查詢數(shù)據(jù),查詢到了就更新,沒(méi)查詢到就插入
  Future<void> _insertOrUpdate(Map<String, dynamic> args) async {
    int id = args['id'];
    String data = args['data'];
    Item? item = await _selected(id);
    if (item == null) {
      await _insert(data);
    } else {
      await _update(data);
    }
  }

  Future<Item?> insertOrUpdate(int id, String data) async {
    return await _taskQueue.submit(_insertOrUpdate, {
      'id': id,
      'data': data,
    });
  }
}

void main() async {
  DBTest.instance.insertOrUpdate(1, 'data');
  DBTest.instance.insertOrUpdate(1, 'data');
}

輸出日志也變成了我們期望的樣子:

執(zhí)行了插入
執(zhí)行了更新

總結(jié)

  • Flutter 異步方法修改數(shù)據(jù)時(shí), 一定要注意數(shù)據(jù)操作的原子性, 不能因?yàn)?Flutter 是單線程架構(gòu),就忽略多個(gè)異步方法競(jìng)爭(zhēng)導(dǎo)致數(shù)據(jù)異常的問(wèn)題。

  • Flutter 保證數(shù)據(jù)操作的原子性,也有可行辦法,當(dāng)邏輯比較簡(jiǎn)單時(shí),可直接使用 Completer,當(dāng)邏輯比較復(fù)雜時(shí),可以考慮使用任務(wù)隊(duì)列。

另外,本文中的任務(wù)隊(duì)列實(shí)現(xiàn)有很大的缺陷,不支持 submit await submit,否則整個(gè)任務(wù)隊(duì)列會(huì)被阻塞住。

到此這篇關(guān)于Flutter如何保證數(shù)據(jù)操作原子性的文章就介紹到這了,更多相關(guān)Flutter數(shù)據(jù)操作原子性內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Android開(kāi)心消消樂(lè)代碼實(shí)例詳解

    Android開(kāi)心消消樂(lè)代碼實(shí)例詳解

    這篇文章主要介紹了Android開(kāi)心消消樂(lè)代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-05-05
  • android仿QQ個(gè)人主頁(yè)下拉回彈效果

    android仿QQ個(gè)人主頁(yè)下拉回彈效果

    這篇文章主要為大家詳細(xì)介紹了android仿QQ個(gè)人主頁(yè)下拉回彈效果的相關(guān)資料,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-02-02
  • AndroidStudio重新share代碼和上傳到svn新地址教程

    AndroidStudio重新share代碼和上傳到svn新地址教程

    這篇文章主要介紹了AndroidStudio重新share代碼和上傳到svn新地址教程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2020-04-04
  • Android布局耗時(shí)監(jiān)測(cè)的三種實(shí)現(xiàn)方式

    Android布局耗時(shí)監(jiān)測(cè)的三種實(shí)現(xiàn)方式

    在Android應(yīng)用開(kāi)發(fā)中,性能優(yōu)化是一個(gè)至關(guān)重要的方面,為了更好地監(jiān)測(cè)布局渲染的耗時(shí),我們需要一種可靠的實(shí)現(xiàn)方案,本文將介紹三種針對(duì)Android布局耗時(shí)監(jiān)測(cè)的實(shí)現(xiàn)方案,幫助開(kāi)發(fā)者及時(shí)發(fā)現(xiàn)并解決布局性能問(wèn)題,需要的朋友可以參考下
    2024-03-03
  • android module解耦組件化總體概述(推薦)

    android module解耦組件化總體概述(推薦)

    這篇文章主要介紹了android module解耦組件化總體概述(推薦),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-07-07
  • Android自定義控件下拉刷新實(shí)例代碼

    Android自定義控件下拉刷新實(shí)例代碼

    這篇文章主要介紹了Android自定義控件下拉刷新實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下
    2016-10-10
  • Android應(yīng)用實(shí)現(xiàn)點(diǎn)擊按鈕震動(dòng)

    Android應(yīng)用實(shí)現(xiàn)點(diǎn)擊按鈕震動(dòng)

    這篇文章主要為大家詳細(xì)介紹了Android應(yīng)用實(shí)現(xiàn)點(diǎn)擊按鈕震動(dòng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-09-09
  • Android仿IOS回彈效果 支持任何控件

    Android仿IOS回彈效果 支持任何控件

    這篇文章主要為大家詳細(xì)介紹了Android仿IOS回彈效果,支持任何控件,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2019-01-01
  • android圖像繪制(七)ClipRect局部繪圖/切割原圖繪制總結(jié)

    android圖像繪制(七)ClipRect局部繪圖/切割原圖繪制總結(jié)

    這幾天開(kāi)始學(xué)游戲地圖制作,今天小小的總結(jié)一下Canvas的clipRect()接口的使用,接下來(lái)介紹ClipRect局部繪圖/切割原圖繪制感興趣的朋友可以了解下
    2013-01-01
  • Android 靜默安裝和卸載的方法

    Android 靜默安裝和卸載的方法

    本篇文章主要介紹了Android 靜默安裝和卸載的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2017-09-09

最新評(píng)論