本文将为您提供关于Android线程池基础入门和简单实践以及使用技巧,这原因我服了的详细介绍,我们还将为您解释android线程池的使用的相关知识,同时,我们还将为您提供关于01.Android线程池
本文将为您提供关于Android线程池基础入门和简单实践以及使用技巧,这原因我服了的详细介绍,我们还将为您解释android线程池的使用的相关知识,同时,我们还将为您提供关于01.Android线程池实践基础、2021年GitHub上那些优秀Android开源库总结,这原因我服了、Android Library上传到JCenter仓库实践,android开发入门和实战、Android 线程池的类型、区别以及为何要用线程池的实用信息。
本文目录一览:- Android线程池基础入门和简单实践以及使用技巧,这原因我服了(android线程池的使用)
- 01.Android线程池实践基础
- 2021年GitHub上那些优秀Android开源库总结,这原因我服了
- Android Library上传到JCenter仓库实践,android开发入门和实战
- Android 线程池的类型、区别以及为何要用线程池
Android线程池基础入门和简单实践以及使用技巧,这原因我服了(android线程池的使用)
开头
很多人工作了十年,但只是用一年的工作经验做了十年而已。
高级工程师一直是市场所需要的,然而很多初级工程师在进阶高级工程师的过程中一直是一个瓶颈。
移动研发在最近两年可以说越来越趋于稳定,因为越来越多人开始学习Android开发,造成市场参差不齐。正所谓入门容易成长很难,对未来比较迷茫,不知道自己技能该怎么提升,并且对于初级中级高级需要怎么进行成才,很多人都比较迷惑。
做了6年Android开发,你会的还只有初级工程师的技术吗?掌握了高级工程师的技术了吗?
一、Java中高级
1、谈谈对java多态的理解?
2、你所知道的设计模式有哪些?
3、静态代理和动态代理的区别,什么场景使用?
5、简单工厂、工厂方法、抽象工厂、Builder模式的区别?
6、装饰模式和代理模式有哪些区别 ?与桥接模式相比呢?
7、集合框架,list,map,set都有哪些具体的实现类,区别都是什么?
8、HashMap和HashTable的主要区别是什么?,两者底层实现的数据结构是什么?
9、HashMap、ConcurrentHashMap、hash()相关原理解析?
10、说说你对Java反射的理解?
11、介绍一下java中的泛型,泛型擦除以及相关的概念,解析与分派?
12、Java的char是两个字节,是怎么存Utf-8的字符的?
13、是否对字符串在内存当中的存储形式有深入了解
14、java虚拟机指令的认识(高级)
15、什么是线程池,如何使用?为什么要使用线程池?
16、synchronized的原理?
17、多线程的使用场景?
18、JVM的内存模型的理解?
19、描述一下GC的原理和回收策略?
20、类的加载器,双亲机制,Android的类加载器。
21、Java的虚拟机JVM的两个内存:栈内存和堆内存的区别是什么?
22、JVM、Art、Dalvik区别、对比
23、JVM调优的常见命令行工具有哪些?JVM常见的调优参数有哪些?
…
二、计算机网络
1、HTTP与HTTPS有什么区别?
2、HTTP2.0和HTTP1.X相比的新特性
3、Https 请求慢的解决办法
4、Http的request和response的协议组成
5、谈谈对http缓存的了解。
6、Https加密原理。
7、客户端如何校验 CA 证书?
8、HTTPS 中的 SSL 握手建立过程
9、HTTPS 如何防范中间人攻击?
10、为什么tcp要经过三次握手,四次挥手?
11、TCP可靠传输原理实现(滑动窗口)。
12、Tcp和Udp的区别?
13、socket断线重连怎么实现,心跳机制又是怎样实现?
14、Cookie与Session的作用和原理。
15、浏览器输入地址到反馈结果发生了什么?
…
三、性能优化
1、做过哪些APP稳定性方面优化?
2、业务稳定性如何保障?
3、如果发生了异常情况,怎么快速止损?
4、启动优化是怎么做的?
5、是怎么异步的,异步遇到问题没有?
6、版本迭代导致的启动变慢有好的解决方式吗?
7、做内存优化最大的感受是什么?
8、如何避免内存抖动?(代码注意事项)
9、如何解决内存泄漏
10、做布局优化的过程中用到了哪些工具?
11、布局为什么会导致卡顿,你是如何优化的?
12、怎么样自动化获取卡顿信息?
13、TextView setText耗时的原因,对TextView绘制层源码的理解?
14、说一下移动端获取网络数据优化的几个点
15、提高app安全性的方法有哪些?
16、安卓的app加固如何做?
17、安卓的混淆原理是什么?
18、谈谈你对安卓签名的理解。
19、如何优化自定义view
20、TraceView的实现原理,分析数据误差来源。
…
四、第三方库
1、网络底层框架:OkHttp实现原理
2、OKhttp针对网络层有哪些优化?
3、网络请求缓存处理,okhttp如何处理网络缓存的?
4、从网络加载一个10M的图片,说下注意事项?
5、网络封装框架:Retrofit实现原理
6、响应式编程框架:RxJava实现原理
7、图片加载框架:Glide实现原理
8、Glide如何确定图片加载完毕?
9、Glide内存缓存如何控制大小?
10、加载bitmap过程(怎样保证不产生内存溢出)
11、Android中软引用与弱引用的应用场景。
12、LruCache原理
13、Fresco与Glide的对比:
14、Bitmap如何处理大图,如一张30M的大图,如何预防OOM?
15、事件总线框架EventBus实现原理
16、内存泄漏检测框架:LeakCanary实现原理
17、leakCannary中如何判断一个对象是否被回收?如何触发手动gc?c层实现?
18、依赖注入框架:ButterKnife实现原理
19、依赖全局管理框架:Dagger2实现原理
20、数据库框架:GreenDao实现原理
…
五、Android Framework相关
1、简单说下Android系统架构
2、View的事件分发机制?滑动冲突怎么解决?
3、如何解决View的事件冲突?举个开发中遇到的例子?
4、View的绘制流程?
5、LinearLayout的onLayout方法实现解析(layoutVertical核心源码)
6、Requestlayout,onlayout,onDraw,DrawChild区别与联系?
7、Android中进程和线程的关系?区别?
8、如何开启多进程?应用是否可以开启N个进程?
9、Android中IPC方式、各种方式优缺点?为何需要IPC?多进程通信可能会出现的问题?
10、讲讲AIDL?如何优化多模块都使用AIDL的情况?
11、为什么选择Binder?Binder机制的作用和原理?
12、Binder框架中ServiceManager的作用?
13、Android系统启动流程是什么?
14、AMS家族
15、App启动流程(Activity的冷启动流程)
16、ActivityThread工作原理?
17、说下四大组件的启动过程,四大组件的启动与销毁的方式。
18、广播发送和接收的原理
19、AMS是如何管理Activity的?
20、大体说下一个应用程序安装到手机上时发生了什么?
21、Android的打包流程?
六、其他高频面试题
1、MVC MVP MVVM原理和区别?
2、可以说一下MVC->MVP->MVVM演进过程吗?
3、热修复原理?插件化原理?
4、ARouter路由原理?
5、Gradle生命周期
6、如何保证一个后台服务不被杀死?比较省电的方式是什么?
7、Android动画框架实现原理。
8、低版本SDK如何实现高版本api?
9、说说你对Context的理解?
10、Android的生命周期和启动模式
11、ListView和RecyclerView系列
12、如何实现一个推送,消息推送原理?推送到达率的问题?
13、对谷歌新推出的Room架构有了解吗?
14、Debug跟Release的APK的区别?
15、Android中进程内存的分配,能不能自己分配定额内存?
16、SurfaceView和View的最本质的区别?
17、曲面屏的适配。
18、TextView调用setText方法的内部执行流程。
19、怎么控制另外一个进程的View显示(RemoteView)?
20、对文件描述符怎么理解?
由于篇幅原因,这份面试宝典已经被整理成了PDF文档,有需要Android面试宝典全套完整文档的麻烦点赞+点击这里即可获取资料免费领取方式!
本文在开源项目:腾讯文档中已收录,里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
[外链图片转存中…(img-2L7iaraK-1617952319304)]
本文在开源项目:腾讯文档中已收录,里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
01.Android线程池实践基础
目录介绍
- 01.实际开发问题
- 02.线程池的优势
- 03.ThreadPoolExecutor参数
- 04.ThreadPoolExecutor使用
- 05.线程池执行流程
- 06.四种线程池类
- 07.execute和submit区别
- 08.线程池的使用技巧
01.实际开发问题
- 在我们的开发中经常会使用到多线程。例如在Android中,由于主线程的诸多限制,像网络请求等一些耗时的操作我们必须在子线程中运行。
- 我们往往会通过new Thread来开启一个子线程,待子线程操作完成以后通过Handler切换到主线程中运行。这么以来我们无法管理我们所创建的子线程,并且无限制的创建子线程,它们相互之间竞争,很有可能由于占用过多资源而导致死机或者OOM。所以在Java中为我们提供了线程池来管理我们所创建的线程。
02.线程池的优势
- ①降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
- ②提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行;
- ③方便线程并发数的管控,线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或oom等状况,从而降低系统的稳定性。线程池能有效管控线程,统一分配、调优,提供资源使用率;
- ④更强大的功能,线程池提供了定时、定期以及可控线程数等功能的线程池,使用方便简单。
03.ThreadPoolExecutor
- 可以通过ThreadPoolExecutor来创建一个线程池。
ExecutorService service = new ThreadPoolExecutor(....);
- 下面我们就来看一下ThreadPoolExecutor中的一个构造方法。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
- ThreadPoolExecutor参数含义
- 1.corePoolSize
- 线程池中的核心线程数,默认情况下,核心线程一直存活在线程池中,即便他们在线程池中处于闲置状态。除非我们将ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这时候处于闲置的核心线程在等待新任务到来时会有超时策略,这个超时时间由keepAliveTime来指定。一旦超过所设置的超时时间,闲置的核心线程就会被终止。
- 2.maximumPoolSize
- 线程池中所容纳的最大线程数,如果活动的线程达到这个数值以后,后续的新任务将会被阻塞。包含核心线程数+非核心线程数。
- 3.keepAliveTime
- 非核心线程闲置时的超时时长,对于非核心线程,闲置时间超过这个时间,非核心线程就会被回收。只有对ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这个超时时间才会对核心线程产生效果。
- 4.unit
- 用于指定keepAliveTime参数的时间单位。他是一个枚举,可以使用的单位有天(TimeUnit.DAYS),小时(TimeUnit.HOURS),分钟(TimeUnit.MINUTES),毫秒(TimeUnit.MILLISECONDS),微秒(TimeUnit.MICROSECONDS, 千分之一毫秒)和毫微秒(TimeUnit.NANOSECONDS, 千分之一微秒);
- 5.workQueue
- 线程池中保存等待执行的任务的阻塞队列。通过线程池中的execute方法提交的Runable对象都会存储在该队列中。我们可以选择下面几个阻塞队列。我们还能够通过实现BlockingQueue接口来自定义我们所需要的阻塞队列。 | 阻塞队列 | 说明 | | ------- | -------- | | ArrayBlockingQueue | 基于数组实现的有界的阻塞队列,该队列按照FIFO(先进先出)原则对队列中的元素进行排序。| | LinkedBlockingQueue | 基于链表实现的阻塞队列,该队列按照FIFO(先进先出)原则对队列中的元素进行排序。| | SynchronousQueue | 内部没有任何容量的阻塞队列。在它内部没有任何的缓存空间。对于SynchronousQueue中的数据元素只有当我们试着取走的时候才可能存在。| | PriorityBlockingQueue | 具有优先级的无限阻塞队列。|
- 6.threadFactory
- 线程工厂,为线程池提供新线程的创建。ThreadFactory是一个接口,里面只有一个newThread方法。 默认为DefaultThreadFactory类。
- 7.handler
- 是RejectedExecutionHandler对象,而RejectedExecutionHandler是一个接口,里面只有一个rejectedExecution方法。当任务队列已满并且线程池中的活动线程已经达到所限定的最大值或者是无法成功执行任务,这时候ThreadPoolExecutor会调用RejectedExecutionHandler中的rejectedExecution方法。在ThreadPoolExecutor中有四个内部类实现了RejectedExecutionHandler接口。在线程池中它默认是AbortPolicy,在无法处理新任务时抛出RejectedExecutionException异常。
- 下面是在ThreadPoolExecutor中提供的四个可选值。
- 我们也可以通过实现RejectedExecutionHandler接口来自定义我们自己的handler。如记录日志或持久化不能处理的任务。 | 可选值 | 说明 | | ----- | ------- | | CallerRunsPolicy | 只用调用者所在线程来运行任务。| | AbortPolicy | 直接抛出RejectedExecutionException异常。| | DiscardPolicy | 丢弃掉该任务,不进行处理。| | DiscardOldestPolicy | 丢弃队列里最近的一个任务,并执行当前任务。|
- 如下图所示
04.ThreadPoolExecutor使用
- 如下所示
ExecutorService service = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
- 对于ThreadPoolExecutor有多个构造方法,对于上面的构造方法中的其他参数都采用默认值。可以通过execute和submit两种方式来向线程池提交一个任务。
- execute
- 当我们使用execute来提交任务时,由于execute方法没有返回值,所以说我们也就无法判定任务是否被线程池执行成功。
service.execute(new Runnable() { public void run() { System.out.println("execute方式"); } });
- submit
- 当我们使用submit来提交任务时,它会返回一个future,我们就可以通过这个future来判断任务是否执行成功,还可以通过future的get方法来获取返回值。如果子线程任务没有完成,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时候有可能任务并没有执行完。
Future<Integer> future = service.submit(new Callable<Integer>() { @Override public Integer call() throws Exception { System.out.println("submit方式"); return 2; } }); try { Integer number = future.get(); } catch (ExecutionException e) { e.printStackTrace(); }
- 线程池关闭
- 调用线程池的
shutdown()
或shutdownNow()
方法来关闭线程池 - shutdown原理:将线程池状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
- shutdownNow原理:将线程池的状态设置成STOP状态,然后中断所有任务(包括正在执行的)的线程,并返回等待执行任务的列表。
- 中断采用interrupt方法,所以无法响应中断的任务可能永远无法终止。 但调用上述的两个关闭之一,isShutdown()方法返回值为true,当所有任务都已关闭,表示线程池关闭完成,则isTerminated()方法返回值为true。当需要立刻中断所有的线程,不一定需要执行完任务,可直接调用shutdownNow()方法。
- 调用线程池的
05.线程池执行流程
- 大概的流程图如下
- 文字描述如下
- ①如果在线程池中的线程数量没有达到核心的线程数量,这时候就回启动一个核心线程来执行任务。
- ②如果线程池中的线程数量已经超过核心线程数,这时候任务就会被插入到任务队列中排队等待执行。
- ③由于任务队列已满,无法将任务插入到任务队列中。这个时候如果线程池中的线程数量没有达到线程池所设定的最大值,那么这时候就会立即启动一个非核心线程来执行任务。
- ④如果线程池中的数量达到了所规定的最大值,那么就会拒绝执行此任务,这时候就会调用RejectedExecutionHandler中的rejectedExecution方法来通知调用者。
06.四种线程池类
- Java中四种具有不同功能常见的线程池。
- 他们都是直接或者间接配置ThreadPoolExecutor来实现他们各自的功能。这四种线程池分别是newFixedThreadPool,newCachedThreadPool,newScheduledThreadPool和newSingleThreadExecutor。这四个线程池可以通过Executors类获取。
6.1 newFixedThreadPool
- 通过Executors中的newFixedThreadPool方法来创建,该线程池是一种线程数量固定的线程池。
ExecutorService service = Executors.newFixedThreadPool(4);
- 在这个线程池中 所容纳最大的线程数就是我们设置的核心线程数。
- 如果线程池的线程处于空闲状态的话,它们并不会被回收,除非是这个线程池被关闭。如果所有的线程都处于活动状态的话,新任务就会处于等待状态,直到有线程空闲出来。
- 由于newFixedThreadPool只有核心线程,并且这些线程都不会被回收,也就是它能够更快速的响应外界请求 。
- 从下面的newFixedThreadPool方法的实现可以看出,newFixedThreadPool只有核心线程,并且不存在超时机制,采用LinkedBlockingQueue,所以对于任务队列的大小也是没有限制的。
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
6.2 newCachedThreadPool
- 通过Executors中的newCachedThreadPool方法来创建。
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
- 通过s上面的newCachedThreadPool方法在这里我们可以看出它的 核心线程数为0, 线程池的最大线程数Integer.MAX_VALUE。而Integer.MAX_VALUE是一个很大的数,也差不多可以说 这个线程池中的最大线程数可以任意大。
- 当线程池中的线程都处于活动状态的时候,线程池就会创建一个新的线程来处理任务。该线程池中的线程超时时长为60秒,所以当线程处于闲置状态超过60秒的时候便会被回收。
- 这也就意味着若是整个线程池的线程都处于闲置状态超过60秒以后,在newCachedThreadPool线程池中是不存在任何线程的,所以这时候它几乎不占用任何的系统资源。
- 对于newCachedThreadPool他的任务队列采用的是SynchronousQueue,上面说到在SynchronousQueue内部没有任何容量的阻塞队列。SynchronousQueue内部相当于一个空集合,我们无法将一个任务插入到SynchronousQueue中。所以说在线程池中如果现有线程无法接收任务,将会创建新的线程来执行任务。
6.3 newScheduledThreadPool
- 通过Executors中的newScheduledThreadPool方法来创建。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); }
- 它的核心线程数是固定的,对于非核心线程几乎可以说是没有限制的,并且当非核心线程处于限制状态的时候就会立即被回收。
- 创建一个可定时执行或周期执行任务的线程池:
ScheduledExecutorService service = Executors.newScheduledThreadPool(4); service.schedule(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+"延迟三秒执行"); } }, 3, TimeUnit.SECONDS); service.scheduleAtFixedRate(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+"延迟三秒后每隔2秒执行"); } }, 3, 2, TimeUnit.SECONDS);
- 输出结果:
pool-1-thread-2延迟三秒后每隔2秒执行 <br>pool-1-thread-1延迟三秒执行 <br>pool-1-thread-1延迟三秒后每隔2秒执行 <br>pool-1-thread-2延迟三秒后每隔2秒执行 <br>pool-1-thread-2延迟三秒后每隔2秒执行
- 部分方法说明
schedule(Runnable command, long delay, TimeUnit unit)
:延迟一定时间后执行Runnable任务;schedule(Callable callable, long delay, TimeUnit unit)
:延迟一定时间后执行Callable任务;scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
:延迟一定时间后,以间隔period时间的频率周期性地执行任务;scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,TimeUnit unit)
:与scheduleAtFixedRate()方法很类似,但是不同的是scheduleWithFixedDelay()方法的周期时间间隔是以上一个任务执行结束到下一个任务开始执行的间隔,而scheduleAtFixedRate()方法的周期时间间隔是以上一个任务开始执行到下一个任务开始执行的间隔,也就是这一些任务系列的触发时间都是可预知的。
- ScheduledExecutorService功能强大,对于定时执行的任务,建议多采用该方法。
6.4 newSingleThreadExecutor
- 通过Executors中的newSingleThreadExecutor方法来创建,在这个线程池中只有一个核心线程,对于任务队列没有大小限制,也就意味着这一个任务处于活动状态时,其他任务都会在任务队列中排队等候依次执行。
- newSingleThreadExecutor将所有的外界任务统一到一个线程中支持,所以在这个任务执行之间我们不需要处理线程同步的问题。
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
07.execute和submit区别
- 先思考一个问题
- 为了保证项目中线程数量不会乱飙升,不好管理,我们会使用线程池,保证线程在我们的管理之下。
- 我们也经常说:使用线程池复用线程。那么问题是:线程池中的线程是如何复用的?是执行完成后销毁,再新建几个放那;还是始终是那几个线程(针对 coreSize 线程)。
- execute和submit
- 调用线程池的execute方法(ExecutorService的submit方法最终也是调用execute)传进去的Runnable,并不会直接以new Thread(runnable).start()的方式来执行,而是通过一个正在运行的线程来调用我们传进去的Runnable的run方法的。
- 那么,这个正在运行的线程,在执行完传进去的Runnable的run方法后会销毁吗?看情况。
- 大部分场景下,我们都是通过Executors的newXXX方法来创建线程池的,就拿newCachedThreadPool来说:
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
- 看第三个参数(keepAliveTime):60L,后面的单位是秒,也就是说,newCachedThreadPool方法返回的线程池,它的工作线程(也就是用来调用Runnable的run方法的线程)的空闲等待时长为60秒,如果超过了60秒没有获取到新的任务,那么这个工作线程就会结束。如果在60秒内接到了新的任务,那么它会在新任务结束后重新等待。
- 还有另一种常用的线程池,通过newFixedThreadPool方法创建的:
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
- 它跟上面的newCachedThreadPool方法一样,创建的都是ThreadPoolExecutor的对象,只是参数不同而已。 可以看到第三个参数设置成了0,这就说明,如果当前工作线程数 > corePoolSize时,并且有工作线程在执行完上一个任务后没拿到新的任务,那么这个工作线程就会立即结束。 再看第二个参数(maximumPoolSize),它设置成了跟corePoolSize一样大,也就是说当前工作线程数 永远不会大于 corePoolSize了,这样的话,即使有工作线程是空闲的,也不会主动结束,会一直等待下一个任务的到来。
- ThreadPoolExecutor分析
- 来探究一下ThreadPoolExecutor是如何管理线程的,先来看精简后的execute方法:
- 逻辑很清晰:当execute方法被调用时,如果当前工作线程 < corePoolSize(上面ThreadPoolExecutor构造方法的第一个参数)的话,就会创建新的线程,否则加入队列。加入队列后如果没有工作线程在运行,也会创建一个。
private final BlockingQueue<Runnable> workQueue; public void execute(Runnable command) { int c = ctl.get(); //当前工作线程还没满 if (workerCountOf(c) < corePoolSize) { //可以创建新的工作线程来执行这个任务 if (addWorker(command, true)){ //添加成功直接返回 return; } } //如果工作线程满了的话,会加入到阻塞队列中 if (workQueue.offer(command)) { int recheck = ctl.get(); //加入到队列之后,如果当前没有工作线程,那么就会创建一个工作线程 if (workerCountOf(recheck) == 0) addWorker(null, false); } }
- 接着看它是怎么创建新线程的:
- 主要操作是再次检查,然后创建Worker对象,并且把worker对象店家到HashSet集合中,最后启动工作线程。
private final HashSet<Worker> workers = new HashSet<>(); private boolean addWorker(Runnable firstTask, boolean core) { //再次检查 int wc = workerCountOf(c); if (wc >= CAPACITY || wc >= corePoolSize) return false; boolean workerStarted = false; Worker w = null; //创建Worker对象 w = new Worker(firstTask); //添加到集合中 workers.add(w); final Thread t = w.thread; //启动工作线程 t.start(); workerStarted = true; return workerStarted; }
- 看看Worker里面是怎么样的:
- 可以看到,这个Worker也是一个Runnable。构造方法里面还创建了一个Thread,这个Thread对象,对应了上面addWorker方法启动的那个thread。
private final class Worker extends AbstractQueuedSynchronizer implements Runnable { final Thread thread; Runnable firstTask; Worker(Runnable firstTask) { this.firstTask = firstTask; this.thread = getThreadFactory().newThread(this); } public void run() { runWorker(this); } }
- 再看Worker类中的run方法,它调用了runWorker,并把自己传了进去:
- Worker里面的firstTask,就是我们通过execute方法传进去的Runnable,可以看到它会在这个方法里面被执行。
- 执行完成之后,接着就会通过getTask方法尝试从等待队列中(上面的workQueue)获取下一个任务,如果getTask方法返回null的话,那么这个工作线程就会结束。
final void runWorker(Worker w) { Runnable task = w.firstTask; w.firstTask = null; while (task != null || (task = getTask()) != null) { try { task.run(); } finally { task = null; w.completedTasks++; } } }
- 最后看看runWorker方法中的getTask方法
private Runnable getTask() { boolean timedOut = false; // Did the last poll() time out? for (; ; ) { int c = ctl.get(); int wc = workerCountOf(c); //如果当前工作线程数大于指定的corePoolSize的话,就要视情况结束工作线程 boolean timed = wc > corePoolSize; //(当前工作线程数 > 指定的最大线程数 || (工作线程数 > 指定的核心线程数 && 上一次被标记超时了)) && (当前工作线程数有2个以上 || 等待队列现在是空的) if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) { return null; } //如果当前工作线程数大于指定的corePoolSize,就看能不能在keepAliveTime时间内获取到新任务 //如果线程数没有 > corePoolSize的话,就会一直等待 Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take(); if (r != null) return r; //没能在keepAliveTime时间内获取到新任务,标记已超时 timedOut = true; } }
08.线程池的使用技巧
- 需要针对具体情况而具体处理,不同的任务类别应采用不同规模的线程池,任务类别可划分为CPU密集型任务、IO密集型任务和混合型任务。(N代表CPU个数) | 任务类别 | 说明 | | ------ | ----------- | | CPU密集型任务 | 线程池中线程个数应尽量少,如配置N+1个线程的线程池。| | IO密集型任务 | 由于IO操作速度远低于CPU速度,那么在运行这类任务时,CPU绝大多数时间处于空闲状态,那么线程池可以配置尽量多些的线程,以提高CPU利用率,如2*N。| | 混合型任务 | 可以拆分为CPU密集型任务和IO密集型任务,当这两类任务执行时间相差无几时,通过拆分再执行的吞吐率高于串行执行的吞吐率,但若这两类任务执行时间有数据级的差距,那么没有拆分的意义。 |
Android线程池实践库:https://github.com/yangchong211/YCThreadPool
2021年GitHub上那些优秀Android开源库总结,这原因我服了
前言
随着移动网络的不断升级,客户端的网络传输由3G进化到Wifi、4G、5G,且Wifi场景越来越多。虽然网络环境在变好,但也对网络的应用提出了更高的要求,会发现很多大厂都十分重视网络指标,如果技术人员不加以控制,在弱网、体验、包括服务器带宽、流浪方面都会造成不同程度的损失。
前几天和我一个在阿里的朋友闲谈的时候,特意问了这位10年Android开发的“骨灰级”面试官,阿里巴巴面试都问哪些原理问题。特此整理出这篇文章,希望对大家的面试有所帮助。
BATJ大场面试必问Android原理问题
- HashMap原理
- ConcurrentHashMap 的实现原理
- HashTable 实现原理
- 广播发送和接收的原理了解吗?(Binder 机制、AMS)
- 传统IPC 机制的通信原理(2 次内存拷贝)
- Binder 机制的作用和原理?
- LRUCache 原理
- 图片加载原理
- 讲讲AIDL?原理是什么?
- 说一下泛型原理,并举例说明
- Handler 的原理
- Handler 引起的内存泄露原因以及最佳解决方案
- Looper 死循环为什么不会导致应用卡死
- AsyncTask 的原理
- ThreadLocal 的原理
- 什么是ANR ? 什么情况会出现ANR ?如何避免?
- 讲解一下Context
- java 中的线程创建方式,线程池的工作原理
- 线程同步机制与原理,举例说明
- 内存回收机制与GC 算法;GC 原理时机以及GC 对象
- 热修复原理
- 插件化原理分析
- 组建化原理
- ARouter 路由原理:
- 区别Animation 和Animator 的用法,概述其原理
- 用过哪些网络加载库?OkHttp、Retrofit 实现原理?
- volatile 的原理
- synchronized 原理
- 补间动画实现原理
- MVC MVP MVVM 原理和区别
大厂面试必问算法题
1.排序算法有哪些?
2.最快的排序算法是哪个?
3.手写一个冒泡排序
4.手写快速排序代码
5.快速排序的过程、时间复杂度、空间复杂度
6.手写堆排序
7.堆排序过程、时间复杂度及空间复杂度
8.写出你所知道的排序算法及时空复杂度,稳定性
9.二叉树给出根节点和目标节点,找出从根节点到目标节点的路径
10 给阿里2 万多名员工按年龄排序应该选择哪个算法?
11.GC 算法(各种算法的优缺点以及应用场景)
12.蚁群算法与蒙特卡洛算法
13.子串包含问题(KMP 算法)写代码实现
14 一个无序,不重复数组,输出N 个元素,使得N 个元素的和相加为M,给出时间复杂度、.
空间复杂度。手写算法
15.万亿级别的两个URL 文件A 和B,如何求出A 和B 的差集C(提示:Bit 映射->hash 分组->
多文件读写效率->磁盘寻址以及应用层面对寻址的优化)
16.百度POI 中如何试下查找最近的商家功能(提示:坐标镜像+R 树)。
17.两个不重复的数组集合中,求共同的元素。
18.两个不重复的数组集合中,这两个集合都是海量数据,内存中放不下,怎么求共同的元
素?
19.一个文件中有100 万个整数,由空格分开,在程序中判断用户输入的整数是否在此文件
中。说出最优的方法
20.一张Bitmap 所占内存以及内存占用的计算
最后
总而言之,Android开发行业变化太快,作为技术人员就要保持终生学习的态度,让学习力成为核心竞争力,所谓“活到老学到老”只有不断的学习,不断的提升自己,才能跟紧行业的步伐,才能不被时代所淘汰。
在这里我分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司19年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。需要的朋友可以点这里免费领取
还有高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。 领取地址: Android学习PDF+架构视频+最新面试文档+源码笔记
。 领取地址: Android学习PDF+架构视频+最新面试文档+源码笔记
Android Library上传到JCenter仓库实践,android开发入门和实战
这里示例创建一个myutils的包,如下图所示:
ok,这个时候你创建好了package,后面就可以准备我们开发好的Library,然后上传到maven仓库,继续耐心看下面的步骤。
创建一个Android Library
===================
新建一个android project,然后new 一个module,选择android library,项目结构如下:
这里我只是为了演示,创建了一个简单的类:
应用bintray插件
===========
如果想通过bintrayUpload命令上传library就必须引用相应的插件,配置如下:
dependencies {
classpath ‘com.android.tools.build:gradle:2.0.0’
classpath ‘com.github.dcendents:android-maven-gradle-plugin:latest.release’
classpath ‘com.jfrog.bintray.gradle:gradle-bintray-plugin:1.6’
}
然后在我们的Library工程的build.gradle引用插件,如下所示:
apply plugin: ‘com.android.library’
apply plugin: ‘com.github.dcendents.android-maven’
apply plugin: ‘com.jfrog.bintray’
其中com.android.library是android的插件,表示它是一个android library。
应用成功后,你会看到插件生成的task,如下图所示:
通过这个task,我们可以将我们的library上传到maven仓库当中,现在还不行,我们还得继续完成我们的配置。
build.gradle配置
==============
我们需要在library工程的build.gradle配置我们的相应的信息,下面是示例代码:
apply plugin: ‘com.android.library’
apply plugin: ‘com.github.dcendents.android-maven’
apply plugin: ‘com.jfrog.bintray’
// 这个version是区分library版本的,因此当我们需要更新library时记得修改这个version
version = “1.0.0”
android {
compileSdkVersion 19
buildToolsversion “23.0.3”
resourcePrefix “devilwwj_”
defaultConfig {
minSdkVersion 9
targetSdkVersion 21
versionCode 1
versionName version
}
lintOptions {
abortOnError false
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’
}
}
}
dependencies {
compile filetree(dir: ‘libs’, include: [’*.jar’])
testCompile ‘junit:junit:4.12’
}
def siteUrl = ‘https://github.com/devilWwj/Android-Tech’ // 项目的主页
def gitUrl = ‘https://github.com/devilWwj/Android-Tech.git’ // Git仓库的url
group = “com.devilwwj.library” // Maven Group ID for the artifact,一般填你唯一的包名
install {
repositories.mavenInstaller {
// This generates POM.xml with proper parameters
pom {
project {
packaging ‘aar’
// Add your description here
description ‘my utils for test’
name ‘Android Commonly used utils’ //项目描述
url siteUrl
// Set your license
licenses {
license {
name ‘The Apache Software License, Version 2.0’
url ‘http://www.apache.org/licenses/LICENSE-2.0.txt’
}
}
developers {
developer { // 开发者信息
id ‘YOUR_ID’
name ‘YOUR NAME’
email ‘YOUR EMAIL’
}
}
scm {
connection gitUrl
developerConnection gitUrl
url siteUrl
}
}
}
}
}
task sourcesJar(type: Jar) {
from android.sourceSets.main.java.srcDirs
classifier = ‘sources’
}
task javadoc(type: Javadoc) {
options.encoding = ‘UTF-8’
source = ‘src/main/java’
}
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = ‘javadoc’
from javadoc.destinationDir
}
artifacts {
archives javadocJar
archives sourcesJar
}
Properties properties = new Properties()
// 加载本地配置
properties.load(project.rootProject.file(‘local.properties’).newDataInputStream())
bintray {
user = properties.getProperty(“bintray.user”)
key = properties.getProperty(“bintray.apikey”)
configurations = [‘archives’]
pkg {
repo = “maven” //发布到Bintray的那个仓库里,默认账户有四个库,我们这里上传到maven库
name = “myutils” //发布到Bintray上的项目名字
websiteUrl = siteUrl
vcsUrl = gitUrl
licenses = [“Apache-2.0”]
publish = true
}
}
你还需要在local.properties中定义两行代码:
总结
【Android 详细知识点思维脑图(技能树)】
其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。
这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
由于篇幅有限,这里以图片的形式给大家展示一小部分。
详细整理在GitHub:Android架构视频+BAT面试专题PDF+学习笔记
识脉络 + 诸多细节。
由于篇幅有限,这里以图片的形式给大家展示一小部分。
[外链图片转存中…(img-qOGAFS7x-1646135424050)]
详细整理在GitHub:Android架构视频+BAT面试专题PDF+学习笔记
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
Android 线程池的类型、区别以及为何要用线程池
每个 Android 应用进程在创建时,会同时创建一个线程,我们称之为主线程,负责更新 UI 界面以及和处理用户之间的交互,因此,在 Android 中,我们又称之为 UI 线程。一个进程中 UI 线程只有一个,为了不造成界面卡顿、提高用户体验,我们势必要将一些耗时操作交由子线程来执行。
使用子线程的方式主要分两种:
- 直接使用 Thread 和 Runnable 等创建子并使用线程
- 使用线程池创建并使用子线程
线程池是什么
线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新创建一个线程。线程池中线程的数量通常完全取决于可用内存数量和应用程序的需求;每个线程都有被分配一个任务,一旦任务完成了,线程回到池子中并等待下一次分配任务。
一般情况下,推荐使用线程池来创建和使用子线程,不建议使用第一种方式。
为何要用线程池
上面说了,在 Android 开发过程中,建议使用线程池来创建和使用子线程,那么使用线程池的好处有哪些呢?
- 线程池改进了一个应用程序的响应时间。由于线程池中的线程已经准备好且等待被分配任务,应用程序可以直接拿来使用而不用新建一个线程。
- 线程池节省了 CLR 为每个短生存周期任务创建一个完整的线程开销并可以在任务完成后回收资源。
- 线程池根据当前在系统中运行的进程来优化线程片。
- 线程池允许我们开启多个任务而不用为每个线程设置属性。
- 线程池允许我们为正在执行的任务的程序参数传递一个包含状态信息的对象引用。
- 线程池可以用来解决处理一个特定请求最大线程数量限制问题。
- 系统分配给每个应用的线程栈是固定的,使用线程池可以有效地避免线程栈溢出引起的应用崩溃。
Android 中的线程池
Android 中线程池的真正实现是 ThreadPoolExecutor,其构造方法有 5 个,通过一系列参数来配置线程池。下面介绍一个比较常用的构造方法及其参数的含义。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory)
参数 | 含义 |
---|---|
corePoolSize |
int : 线程池的核心线程数,默认情况下,核心线程回一直在线程池中存活,即使他们处于闲置状态。如果将 allowCoreThreadTimeOut 的属性设置为 true,那么闲置的核心线程在等待新任务到来时会有超时策略,这个时间间隔由 keepAliveTime 所指定,当等待时间超过 keepAliveTime 所指定的时长后,核心线程就会被终止。 |
maximumPoolSize |
int : 线程池中允许的线程最大数量,当活动线程达到这个数值后,后续的新任务会被阻塞。 |
keepAliveTime |
long : 非核心线程闲置时的超时时长,超过这个时长,非核心线程就会被回收。当 allowCoreThreadTimeOut 属性被设置为 true 时,该参数同样会作用于核心线程。 |
unit |
TimeUnit : keepAliveTime 参数的时间单位 ,参数为 TimeUnit 的枚举,常见的有 TimeUnit.MILLISECONDS (毫秒)、TimeUnit.SECOND (秒) 等。 |
workQueue |
BlockingQueue : 线程池中的任务队列,通过线程池的 execute 方法提交的 Runnable 对象会存储在这个参数中。 |
threadFactory |
ThreadFactory : 创建线程的线程工厂。ThreadFactory 是一个接口,只有一个方法:Thread newThread (Runnable r)
|
Throws | |
---|---|
IllegalArgumentException |
符合以下任一条件,则抛出此异常: corePoolSize < 0 keepAliveTime < 0 maximumPoolSize <= 0 maximumPoolSize < corePoolSize
|
NullPointerException |
当 workQueue 或者 threadFactory 为 null 时,抛出此异常。 |
ThreadPoolExecutor 执行任务时大致遵循如下规则:
- 如果线程池中的线程数量未达到核心线程的数量,那么会直接启动一个核心线程来执行任务。
- 如果线程池中的线程数量已经达到或超过核心线程的数量,那么任务会被插入到任务队列中等待执行。
- 如果步骤 2 中无法将任务插入到任务队列,则表示任务队列已满。此时,如果线程数量未达到 maximumPoolSize 值,则会立即启动一个非核心线程来执行任务。
- 如果步骤 3 中线程数量大于或等于 maximumPoolSize 值,则拒绝执行次任务,此时 ThreadPoolExecutor 会调用 RejectedExecutionHandler 的 rejectedExecution 来通知调用者。
RejectedExecutionHandler 是线程池持有的一个对象,用于不能由 ThreadPoolExecutor 执行的任务的处理
Android 中线程池的类型及区别
Android 中最常见线程池有四种,分别是:FixedThreadPool、CacheThreadPool、ScheduledThreadPool 以及 SingleThreadPool,它们都直接或间接的通过配置 ThreadPoolExecutor 来实现自己的功能特性。
1. FixedThreadPool
线程数量固定的线程池,通过 Executors 的 newFixedThreadPool 方法创建。有两个重载的创建方法:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
除非线程池被关闭,否则线程不会被回收,即时线程处于空闲状态。如果在所有线程都处于活动状态时提交额外的任务,它们将在队列中等待,直到有一个线程可用为止。由创建方法可知,FixedThreadPool 只有核心线程并且这个核心线程没有超时机制(keepAliveTime 参数为 0L),加上线程不会被回收,因此使用此类线程池可以快速地响应外界的请求。
2. CacheThreadPool
线程数量不定的线程池,通过 Executors 的 newCachedThreadPool 方法创建。有两个重载的创建方法:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(/* corePoolSize*/ 0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(/* corePoolSize*/ 0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
CacheThreadPool 有以下个特征:
- 没有核心线程( corePoolSize 参数为 0 ),只有非核心线程且非核心线程的数量为 Integer.MAX_VALUE,这就相当于非核心线程的数量可以无限大。
- 线程池的线程处于空闲状态时,线程池会重用空闲的线程来处理新任务,否则创建新的线程来处理,新创建的线程会添加到线程池中。这将提高执行许多短期异步任务的程序性能。
- 闲置时间超过 60 秒的空闲线程会被回收(keepAliveTime 参数为 60L )。因此,闲置时间足够长的 CacheThreadPool 也不会消耗任何系统资源。
3. ScheduledThreadPool
核心线程数量固定,非核心线程数量不定的线程池,通过 Executors 的 newscheduledthreadpool 方法创建。有两个重载的创建方法:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
ScheduledThreadPool 相比较其他三种线程池,有特殊性,由 ScheduledThreadPoolExecutor 实现, newscheduledthreadpool 方法也是通过创建 ScheduledThreadPoolExecutor 的实例来完成线程池的创建,代码如下:
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) { super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue(), threadFactory);
}
由 ScheduledThreadPoolExecutor 的构造函数可知, ScheduledThreadPool 的核心线程数量是固定的,由传入的 corePoolSize 参数决定,非核心线程数量可以无限大。非核心线程闲置回收的超时时间为 10秒( DEFAULT_KEEPALIVE_MILLIS 的值为 10L )。这类线程主要用于执行定时任务或者具有周期性的重复任务。
4. SingleThreadPool
只有一个核心线程,通过 Executors 的 newsinglethreadexecutor 方法创建。有两个重载的创建方法:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(/* corePoolSize*/ 1, /* maximumPoolSize*/ 1,
/* keepAliveTime*/ 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(/* corePoolSize*/ 1, /* maximumPoolSize*/ 1,
/* keepAliveTime*/ 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
SingleThreadPool 由 代理类 FinalizableDelegatedExecutorService 创建。该线程池只有一个线程(核心线程),并且该线程池的任务队列是无上限的,这就确保了所有的任务都在同一个线程中顺序执行。
注意,如果由于在执行期间出现故障而导致该线程终止,那么如果需要执行后续任务,则新线程将取而代之。
四类线程池的区别
上面分别对 Android 中常见的 4 种线程池进行了简单的介绍,除了这 4 种系统提供的线程池外,我们在使用的过程中,也可以根据需要直接通过 ThreadPoolExecutor 的构造函数来灵活的配置线程池。那么,上述的 4 种线程池,其区别在哪呢?了解其区别有助于我们去选择更为合适的线程池或者直接通过 ThreadPoolExecutor 来配置更灵活的线程池。
FixedThreadPool 线程固定,且不会被回收,能够更快的响应外界请求。
CachedThreadPool 只有非核心线程,且线程数相当于无限大,任何任务都会被立即执行。比较适合执行大量的耗时较少的任务。
ScheduledThreadPool 主要用于执行定时任务或者具有周期性的重复任务。
SingleThreadPool 只有一个核心线程,确保所有任务都在同一线程中按顺序完成。因此不需要处理线程同步的问题。
参考
- 《Android艺术开发探索》,电子书下载
- Android 官方文档
我们今天的关于Android线程池基础入门和简单实践以及使用技巧,这原因我服了和android线程池的使用的分享已经告一段落,感谢您的关注,如果您想了解更多关于01.Android线程池实践基础、2021年GitHub上那些优秀Android开源库总结,这原因我服了、Android Library上传到JCenter仓库实践,android开发入门和实战、Android 线程池的类型、区别以及为何要用线程池的相关信息,请在本站查询。
本文标签: