后台任务队列管理神器 Android-Priority-Job-Queue

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 有人说“Android的开发,玩的就是多线程”。从某个角度来说的确如此,现在的App被设计的越来越复杂,相信很多开发人员都因大量而又复杂的后台任务(background work)而焦头烂额:Async-Task和Activity的生命周期太过于耦合...

有人说“Android的开发,玩的就是多线程”。从某个角度来说的确如此,现在的App被设计的越来越复杂,相信很多开发人员都因大量而又复杂的后台任务(background work)而焦头烂额:Async-Task和Activity的生命周期太过于耦合,虽然实现简单但是对于重要的后台任务还是不靠谱;Loaders虽然可以用于异步从磁盘列读取数据,但是对于异步的网络请求就无能为力了;相对给力点的方案是后台服务中开辟进程池(Thread Pool),使用ThreadPoolExecutor来帮助管理线程,但是app越复杂后台操作越多,需要处理的多线程的问题越多,想一想就头大.....
但是各位读者不要沮丧,今天就是向大家介绍一个后台任务队列管理库Android-Priority-Job-Queue,它将提供一个优雅的架构来解决以上所有的问题!

1. 简介

用官方的话来说,Android-Priority-Job-Queue是一款专门为Android平台编写的,实现了Job Queue的后台任务队列类库,能够轻松的在后台执行定时任务,并且提高了用户体验和应用的稳定性。其设计理念以灵活性和功能性为主,并且一直在更新。

“Priority Job Queue is an implementation of a Job Queue specifically written for Android to easily schedule jobs (tasks) that run in the background, improving UX and application stability.”
It is written primarily with flexibility & functionality in mind. This is an ongoing project, which we will continue to add stability and performance improvements.

github : https://github.com/yigit/android-priority-jobqueue
在这里可以了解到更多更全面的介绍。

其使用框架也很简便直接:

  1. 构造一个任务管理器JobManager,为我们管理任务;
  2. 自定义Job类,来作为任务的载体;
  3. 在需要时,将自定义的Job类实例加入到JobManager中;

这样就OK了,JobManager会根据优先级、持久性、负载平衡、延迟,网络控制、分组等因素来管理任务的执行。由于是独立于各个Activity,JobManager为Job的执行提供了一个很好的生命周期,用户体验更为棒。是不是很惊喜!闲话少叙,我们来看使用范例吧!

2. 使用实例

2.1 添加方法

在Android Studio添加,如下引用:

dependencies {
    compile 'com.birbit:android-priority-jobqueue:2.0.1'
}

如果你很怀旧,还在坚持使用Eclipse,那就在maven这样配置:

<dependency>
    <groupId>com.birbit</groupId>
    <artifactId>android-priority-jobqueue</artifactId>
    <version>2.0.1</version>
</dependency>

2.2 配置JobManager

JobManager是整个框架的核心。作为一个重型的对象,建议Application只构建一个JobManager实例供全局使用。另一方面,为了让任务的执行有一个更好的生命周期,建议将JobManager放在Application类,而不是一个具体的Activity。以下是示例代码:

import android.app.Application;
import android.util.Log;
import com.path.android.jobqueue.JobManager;
import com.path.android.jobqueue.config.Configuration;
import com.path.android.jobqueue.log.CustomLogger;

public class JobQueueApplication extends Application {
    private JobManager jobManager;
    private static JobQueueApplication instance;
    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;//1. Application的实例
        configureJobManager();//2. 配置JobMananger
    }

   //私有构造器
   private JobQueueApplication(){
        instance=this;
    }
    public JobManager getJobManager() {
        return jobManager;
    }

    public static JobQueueApplication getInstance() {
        return instance;
    }

    private void configureJobManager() {
        //3. JobManager的配置器,利用Builder模式
        Configuration configuration = new Configuration.Builder(this)
                .customLogger(new CustomLogger() {
                    private static final String TAG = "JOBS";
                    @Override
                    public boolean isDebugEnabled() {
                        return true;
                    }

                    @Override
                    public void d(String text, Object... args) {
                        Log.d(TAG, String.format(text, args));
                    }

                    @Override
                    public void e(Throwable t, String text, Object... args) {
                        Log.e(TAG, String.format(text, args), t);
                    }

                    @Override
                    public void e(String text, Object... args) {
                        Log.e(TAG, String.format(text, args));
                    }
                })
                .minConsumerCount(1)//always keep at least one consumer alive
                .maxConsumerCount(3)//up to 3 consumers at a time
                .loadFactor(3)//3 jobs per consumer
                .consumerKeepAlive(120)//wait 2 minute
                .build();
        jobManager = new JobManager(this, configuration);
    }
}

以上是整个自定义Application类的代码,其逻辑很清晰。首先为Application类设置单例模式,并保存私有变量jobManager;然后在onCreate()中调用configureJobManager方法来完成jobManager的初始化。我们来看下在其初始化参数Configuration实例中都配置了哪些内容:

  • CustomLogger:日志设置,便于用户查看任务队列的工作信息,在调试的过程中很有用,后面分析JobManager的任务调度时就会用到;
  • minConsumerCount&maxConsumerCount: 最少消费者和最多消费者数量,所谓的消费者就是开启的线程,用来执行任务。任务队列实际上就是一个生产者和消费者问题,用户是生产者,提交任务(Job),开启的线程就是消费者来执行任务,任务被执行就是“消费”。这里所谓的最少和最大将会下面具体解释;
  • loadFactor(int): 其意义是设置多少个任务为一组被分配个一个消费者(Thread),也就是一个Thread最多要“承包”几个任务来执行;
  • consumerKeepAlive :设置消费者在没有任务的情况下保持存活的时长,以秒为单位,如果过了这个时长还没有任务,消费者线程就会被回收;

Configuration以建筑者模式来链式配置,对此不是很熟悉的读者可以参考如何构建含有大量参数的构造器:浅谈Builder Pattern的使用和链式配置.
有了JobManger自然还需要Job,下面就来看看如何设置Job.

2.3 Job

自定义的Job类需要继承Android-Priority-Job-Queue提供的Job类,下面就是是一个简单的范例,这个任务的内容就是睡眠5秒。

import android.util.Log;
import com.path.android.jobqueue.Job;
import com.path.android.jobqueue.Params;
import com.path.android.jobqueue.RetryConstraint;

public class MyJob extends Job{
    public static final int PRIORITY = 1;
    private String text;
    String TAG = "Myjob";
    int sleepTime;
    public MyJob(String text) {
        // A job should be persisted in case the application exits 
        // before job is completed.
        super(new Params(PRIORITY).persist());
        this.text = text;
        sleepTime = 5;
        Log.i(TAG, text+"  goin");
    }
    @Override
    public void onAdded() {
        // Job has been saved to disk.
        // This is a good place to dispatch a UI event 
        // to indicate the job will eventually run.
        Log.i(TAG, text+"  Onadded");
    }
    @Override
    public void onRun() throws Throwable {
        // Job logic goes here. 
        // All work done here should be synchronous, 
        // a job is removed from the queue once onRun() finishes.
        Thread.sleep(sleepTime*1000);
        Log.i(TAG, text+"  onRun");
    }
    @Override
    protected RetryConstraint shouldReRunOnThrowable(Throwable throwable, int runCount,
                                                     int maxRunCount) {
        // An error occurred in onRun.
        // Return value determines whether this job should retry or cancel. You can further
        // specify a backoff strategy or change the job's priority. You can also apply the
        // delay to the whole group to preserve jobs' running order.
        return RetryConstraint.createExponentialBackoff(runCount, 10);
    }
    @Override
    protected void onCancel() {

    }
}

Job类的模块很清晰,我们只需要按照要求覆盖以下方法即可:

  1. 在构造器,利用Params类中配置参数;
  2. onAdded(): 任务加入队列并被保存在硬盘上,定义此时要处理的逻辑;
  3. onRun(): 任务开始执执行,在此定义任务的主题逻辑,当执行完毕后,任务将被从任务队列中删除;
  4. onCancel():任务取消的时候要执行的逻辑;
  5. shouldReRunOnThrowable():当onRun()方法中抛出异常时,就会调用该函数,该函数返回Job类在执行发生异常时的应对策略,是重新执行还是取消,或者是一定时间之后再尝试。

在这里特别说明下Params类,通过该类可以配置Job类的各种信息,同样也采用类Builder Pattern的链式配置:

  1. 默认构造器传入的是int参数是该任务的优先级,优先级越高,越优先执行。
public Params(int priority) {
        this.priority = priority;
    }
  1. requireNetwork(): 设置该任务要求访问网络;
  2. groupBy(String groupId):设置组ID,被设置相同组ID的任务,将会按照顺序执行;
  3. persist():设置任务为可持久化的,持久化要求Job类为序列化的,这一点并不意外,因为一个类的内容只有序列化之后才能变成字节模式保存在硬盘上;
  4. delayInMs(long delayMs):设置延迟时间,ms为单位,在该时间之后再放入任务队列中。

2.4 执行任务

Job的执行很简单,就把任务类加入到任务队列中即可以。


public class MainActivity extends AppCompatActivity {
    private JobManager jobManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button btn_start = (Button)findViewById(R.id.start_job_button);
        //JobManager对象
        jobManager = JobQueueApplication.getInstance().getJobManager();       
        btn_start.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                for(int i=0; i<9 ;i++) {
                    //将任务加入后台队列中
                    jobManager.addJobInBackground(new MyJob(“"+i));
                } 
            }
        });
    }
}

在一个Activity中点击Button,就会把9个MyJob实例加入到后台队列中。其运行效果如下:

03-25 21:44:47.207 I/Myjob: 0 goin
03-25 21:44:47.208 I/Myjob: 1 goin
03-25 21:44:47.208 I/Myjob: 2 goin
03-25 21:44:47.208 I/Myjob: 3 goin
03-25 21:44:47.208 I/Myjob: 4 goin
03-25 21:44:47.208 I/Myjob: 5 goin
03-25 21:44:47.208 I/Myjob: 6 goin
03-25 21:44:47.208 I/Myjob: 7 goin
03-25 21:44:47.208 I/Myjob: 8 goin
03-25 21:44:47.218 I/Myjob: 0 Onadded
03-25 21:44:47.228 I/Myjob: 1 Onadded
03-25 21:44:47.241 I/Myjob: 2 Onadded
03-25 21:44:47.251 I/Myjob: 3 Onadded
03-25 21:44:47.260 I/Myjob: 4 Onadded
03-25 21:44:47.274 I/Myjob: 5 Onadded
03-25 21:44:47.280 I/Myjob: 6 Onadded
03-25 21:44:47.291 I/Myjob: 7 Onadded
03-25 21:44:47.307 I/Myjob: 8 Onadded
03-25 21:44:52.235 I/Myjob: 1 onRun
03-25 21:44:52.267 I/Myjob: 4 onRun
03-25 21:44:52.297 I/Myjob: 7 onRun
03-25 21:44:57.250 I/Myjob: 8 onRun
03-25 21:44:57.282 I/Myjob: 6 onRun
03-25 21:44:57.310 I/Myjob: 5 onRun
03-25 21:45:02.264 I/Myjob: 3 onRun
03-25 21:45:02.299 I/Myjob: 2 onRun
03-25 21:45:02.324 I/Myjob: 0 onRun

为了便于查看,这里输出的日志信息只保留最主要的内容,我们可以看到任务0-8依次启动并加入到任务队列中,然后再被执行,JobManager帮你管理了一切,完美!
有的读者可能已经发行了,任务0-8的执行顺序是混乱的,这就涉及到Android-Priority-JobQueue任务的调度问题,也是本人最好兴趣的内容。

3. 任务调度分析

回顾一下,我们对JobManager的设置:至少一个消费者线程,至多三个;每个消费者线程最多处理三个任务。这些设置就是在影响任务调度。
当一个任务要加入任务队列时,会做出如下的判断:

  1. 首先计算当前能处理的任务数的最大值(CurrentTaskCapacity)= 已启动的消费线程数(CurrentThreadNum) * 每个线程处理任务的容量(PerThreadTaskCapacity);
CurrentTaskCapacity = CurrentThreadNum * PerThreadTaskCapacity
  1. 如果当前任务数加上现在要加入任务,小于或等于CurrentTaskCapacity,则将该任务加入到任务队列中;
  2. 如果当前任务数加上现在要加入任务,大于CurrentTaskCapacity,则判断CurrentThreadNum是不是已经达到设置的最大值,如果没有就开辟新的消费者线程来承担任务;
  3. 如果CurrentTaskCapacityCurrentThreadNum都达到上限,那就对不起了,该任务就不能加入队列,需要等到有任务执行完毕,任务容量又有了空余时才能进入队列等待执行。

如何验证上面结论呢?还记得我们在Application类中配置JobManager时设置的日志信息吗? 这个时候它就发挥作用了,下面是任务0-8加入到任务队列中时输出的日志,内容很多,请注意的表黑的部分:

03-25 21:44:47.218 D/JOBS: added job id: 84 class: MyJob priority: 0 delay: 0 group : null persistent: true requires network: false
03-25 21:44:47.218 I/Myjob: 0 Onadded
03-25 21:44:47.222 D/JOBS: pool-1-thread-1: load factor check. true = (0 < 1)|| (0 * 3 < 1 + 0). consumer thread: false
03-25 21:44:47.222 D/JOBS: adding another consumer
03-25 21:44:47.223 D/JOBS:** starting consumer Thread-177**
03-25 21:44:47.224 D/JOBS: looking for next job
03-25 21:44:47.224 D/JOBS: running groups
03-25 21:44:47.224 D/JOBS: non persistent result null
03-25 21:44:47.228 D/JOBS: added job id: 85 class: MyJob priority: 1 delay: 0 group : null persistent: true requires network: false
03-25 21:44:47.228 I/Myjob: 1 Onadded
03-25 21:44:47.234 D/JOBS: persistent result com.path.android.jobqueue.JobHolder@55
03-25 21:44:47.234 D/JOBS: running job MyJob
03-25 21:44:47.237 D/JOBS: pool-1-thread-1: load factor check. false = (1 < 1)|| (1 * 3 < 1 + 1). consumer thread: false
03-25 21:44:47.241 D/JOBS: added job id: 86 class: MyJob priority: 2 delay: 0 group : null persistent: true requires network: false
03-25 21:44:47.241 I/Myjob: 2 Onadded
03-25 21:44:47.245 D/JOBS: pool-1-thread-1: load factor check. false = (1 < 1)|| (1 * 3 < 2 + 1). consumer thread: false
03-25 21:44:47.251 D/JOBS: added job id: 87 class: MyJob priority: 3 delay: 0 group : null persistent: true requires network: false
03-25 21:44:47.251 I/Myjob: 3 Onadded
03-25 21:44:47.255 D/JOBS: pool-1-thread-1: load factor check. true=(1 < 1) || (1 * 3 < 3 + 1). consumer thread: false
03-25 21:44:47.255 D/JOBS: adding another consumer
03-25 21:44:47.256 D/JOBS: starting consumer Thread-178
03-25 21:44:47.257 D/JOBS: looking for next job
03-25 21:44:47.257 D/JOBS: running groups
03-25 21:44:47.257 D/JOBS: non persistent result null
03-25 21:44:47.260 D/JOBS: added job id: 88 class: MyJob priority: 4 delay: 0 group : null persistent: true requires network: false
03-25 21:44:47.260 I/Myjob: 4 Onadded
03-25 21:44:47.264 D/JOBS: persistent result com.path.android.jobqueue.JobHolder@58
03-25 21:44:47.264 D/JOBS: running job MyJob
03-25 21:44:47.270 D/JOBS: pool-1-thread-1: load factor check. false = (2 < 1)|| (2 * 3 < 3 + 2). consumer thread: false
03-25 21:44:47.274 D/JOBS: added job id: 89 class: MyJob priority: 5 delay: 0 group : null persistent: true requires network: false
03-25 21:44:47.274 I/Myjob: 5 Onadded
03-25 21:44:47.277 D/JOBS: pool-1-thread-1: load factor check. false = (2 < 1)|| (2 * 3 < 4 + 2). consumer thread: false
03-25 21:44:47.280 D/JOBS: added job id: 90 class: MyJob priority: 6 delay: 0 group : null persistent: true requires network: false
03-25 21:44:47.280 I/Myjob: 6 Onadded
03-25 21:44:47.283 D/JOBS: pool-1-thread-1: load factor check. true = (2 < 1)|| (2 * 3 < 5 + 2). consumer thread: false
03-25 21:44:47.283 D/JOBS: adding another consumer
03-25 21:44:47.287 D/JOBS: starting consumer Thread-179
03-25 21:44:47.288 D/JOBS: looking for next job
03-25 21:44:47.288 D/JOBS: running groups
03-25 21:44:47.288 D/JOBS: non persistent result null
03-25 21:44:47.291 D/JOBS: added job id: 91 class: MyJob priority: 7 delay: 0 group : null persistent: true requires network: false
03-25 21:44:47.291 I/Myjob: 7 Onadded
03-25 21:44:47.295 D/JOBS: persistent result com.path.android.jobqueue.JobHolder@5b
03-25 21:44:47.295 D/JOBS: running job MyJob
03-25 21:44:47.302 D/JOBS: pool-1-thread-1: load factor check. false = (3 < 1)|| (3 * 3 < 5 + 3). consumer thread: false
03-25 21:44:47.306 D/JOBS: added job id: 92 class: MyJob priority: 8 delay: 0 group : null persistent: true requires network: false
03-25 21:44:47.307 I/Myjob: 8 Onadded
03-25 21:44:47.310 D/JOBS: pool-1-thread-1: load factor check. false = (3 < 1)|| (3 * 3 < 6 + 3). consumer thread: false

日志中这样的load factor check. true = (0 < 1)|| (0 * 3 < 1 + 0)计算表达式,就是在进行线程调度的判定,当计算表达式为true时,就意味着要启动新的消费者进程。 ||右边括号内的表达式就是在比较当前任务数和当前任务容量。

通过日志我们可以看到,面对任务0-8,JobManager依次启动了三个消费者进程,并将这9个任务分配给他们:

  • Thread-177:任务0、1、2;
  • Thread-178:任务3、4、5;
  • Thread-179:任务6、7、8;

三个消费者线程并发执行,由于所有任务的优先级都是一样的,消费者线程们就会随机执行任务。

消费者线程被创建,自然也会被回收,这才是完整的生命周期。以下是当任务执行完成时日志的输出:

03-25 21:47:02.280 D/JOBS: Thread-177: load factor check. false = (2 < 1)|| (2 * 3 < 0 + 0). consumer thread: true
03-25 21:47:02.280 D/JOBS: finishing consumer Thread-177
03-25 21:47:02.310 D/JOBS: Thread-178: load factor check. false = (1 < 1)|| (1 * 3 < 0 + 0). consumer thread: true
03-25 21:47:02.310 D/JOBS: finishing consumer Thread-178
03-25 21:47:02.337 D/JOBS: Thread-179: load factor check. true = (0 < 1)|| (0 * 3 < 0 + 0). consumer thread: true
03-25 21:47:02.337 D/JOBS: didn't allow me to die, re-running Thread-179
03-25 21:47:02.337 D/JOBS: re-running consumer Thread-179

我们可以看到,当任务执行完毕之后Thread-177和Thread-178都被回收了,但是由于我们设置了最小消费者线程数为1,所以Thread-179被留下“坚守岗位”,等待下一个任务的到来,直到超时。

由此可知,JobMananger的任务调度机制还是十分复杂和完备的,真庆幸已经有人帮我们实现了。

4. RetryConstraint

最后要说的就是RetryConstraint,即任务在执行中发生异常之后要执行的策略。用户可以根据自己的使用情况来设置:

  1. RETRY: RetryConstraint的自带策略,立刻重新尝试执行策略,直到执行成功或者尝试次数达到最大(18次);
  2. CANCEL:RetryConstraint的自带策略,取消当前任务的执行;
  3. *createExponentialBackoff(int runCount, long initialBackOffInMs) *:定期延迟尝试执行任务,如果任务执行失败,下次执行的延迟时间会以指数形式增长,最大尝试次数为20次;
public static RetryConstraint createExponentialBackoff(int runCount, long initialBackOffInMs) {
        RetryConstraint constraint = new RetryConstraint(true);
        constraint.setNewDelayInMs(initialBackOffInMs *
                (long) Math.pow(2, Math.max(0, runCount - 1)));
        return constraint;
    }

在官方给出的示例PostTweet中,是这样定义* RetryConstraint*的:

    @Override
    protected RetryConstraint shouldReRunOnThrowable(Throwable throwable, int runCount,
            int maxRunCount) {
        if(throwable instanceof TwitterException) {
            //if it is a 4xx error, stop
            TwitterException twitterException = (TwitterException) throwable;
            int errorCode = twitterException.getErrorCode();
            return errorCode < 400 || errorCode > 499 ? RetryConstraint.RETRY : RetryConstraint.CANCEL;
        }
        return RetryConstraint.RETRY;
    }

如果是4XX的错误(服务器错误)就取消访问,其他异常就离开重新尝试请求。

关于Android-Priority-JobQueue,今天先介绍到这里,内容已经够多了。总之,这是一个很优秀的任务队列管理库,很值得使用和研究。欢迎大家尝试,以及给我留言指教。

阅读参考:

  1. Android Priority Job Queue 入门
  2. 如何构建含有大量参数的构造器:浅谈Builder Pattern的使用和链式配置
  3. Java序列化心得(一):序列化设计和默认序列化格式的问题
  4. Java序列化心得(二):自定义序列化
  5. Android Priority Job Queue (Job Manager):线程任务的容错重启机制(二)
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
6月前
|
Java API 调度
Android系统 自定义开机广播,禁止后台服务,运行手动安装应用接收开机广播
Android系统 自定义开机广播,禁止后台服务,运行手动安装应用接收开机广播
336 0
|
3月前
|
编解码 网络协议 Android开发
Android平台GB28181设备接入模块实现后台service按需回传摄像头数据到国标平台侧
我们在做Android平台GB28181设备对接模块的时候,遇到这样的技术需求,开发者希望能以后台服务的形式运行程序,国标平台侧没有视频回传请求的时候,仅保持信令链接,有发起视频回传请求或语音广播时,打开摄像头,并实时回传音视频数据或接收处理国标平台侧发过来的语音广播数据。
|
3月前
|
Android开发 开发者 Kotlin
Android 多进程情况下判断应用是否处于前台或者后台
本文介绍在多进程环境下判断Android应用前后台状态的方法。通过`ActivityManager`和服务信息`RunningAppProcessInfo`可有效检测应用状态,优化资源使用。提供Kotlin代码示例,帮助开发者轻松集成。
275 8
|
4月前
|
监控 Android开发 开发者
Android经典面试题之实战经验分享:如何简单实现App的前后台监听判断
本文介绍在Android中判断应用前后台状态的两种方法:`ActivityLifecycleCallbacks`和`ProcessLifecycleOwner`。前者提供精细控制,适用于需针对每个Activity处理的场景;后者简化前后台检测,适用于多数应用。两者各有优劣:`ActivityLifecycleCallbacks`更精确但复杂度高;`ProcessLifecycleOwner`更简便但可能在极端场景下略有差异。根据应用需求选择合适方法。
41 2
|
6月前
|
缓存 算法 Java
Linux内核新特性年终大盘点-安卓杀后台现象减少的背后功臣MGLRU算法简介
MGLRU是一种新型内存管理算法,它的出现是为了弥补传统LRU(Least Recently Used)和LFU(Least Frequently Used)算法在缓存替换选择上的不足,LRU和LFU的共同缺点就是在做内存页面替换时,只考虑内存页面在最近一段时间内被访问的次数和最后一次的访问时间,但是一个页面的最近访问次数少或者最近一次的访问时间较早,可能仅仅是因为这个内存页面新近才被创建,属于刚刚完成初始化的年代代页面,它的频繁访问往往会出现在初始化之后的一段时间里,那么这时候就把这种年轻代的页面迁移出去
|
5月前
|
Android开发
技术经验分享:Android前后台切换的监听
技术经验分享:Android前后台切换的监听
95 2
|
安全 Java Android开发
Android 中AsyncTask后台线程,异步任务的理解
Android 中AsyncTask后台线程,异步任务的理解
158 0
|
Android开发
使用WakeLock使Android应用程序保持后台唤醒
使用WakeLock使Android应用程序保持后台唤醒
368 0
|
6月前
|
存储 移动开发 JavaScript
【原生】sd.js帮助您简化繁重的获取数据、存储数据(CRUD)骚操作(吐槽~在安卓9.0以下或者IOS10.X以下手机端H5页面不支持,在这两种情况下的系统只能使用ajax或者原生js请求后台数据)
【原生】sd.js帮助您简化繁重的获取数据、存储数据(CRUD)骚操作(吐槽~在安卓9.0以下或者IOS10.X以下手机端H5页面不支持,在这两种情况下的系统只能使用ajax或者原生js请求后台数据)
|
Android开发
Android JetPack组件之ViewModel状态的保存(程序在后台被系统杀死数据也存活)
Android JetPack组件之ViewModel状态的保存(程序在后台被系统杀死数据也存活)
154 0