前言

在实际项目开发中会频繁的用到线程,线程使用起来是很简单,但是滥用线程会带来性能问题, 比如启动一个线程至少 占用16kb的内存、线程过多会导致cpu的频繁切换而cpu切换成本是很高的、消耗大量用户电量等问题, 所以应该让app的线程数保持在合理水平,这是性能优化中很重要的一部分。本文对线程优化方面的知识做了一个全面总结,主要内容如下:

一、线程调度原理解析

线程调度的原理

在任意时刻,CPU 只能执行一条机器指令,每个线程只有获得了 CPU 的使用权之后才能执行指令,也就是说 在任意时刻,只有一个线程占用 CPU,处于运行状态。而我们平常所说的 多线程并发运行,实际上说的是多个线程轮流获取 CPU 的使用权,然后分别执行各自的任务。其实在可运行池当中有多个处于就绪状态的线程在等待 CPU,而 JVM 负责线程调度,按照特定机制为多个线程分配 CPU 使用权。

上面的描述提到了三个主要信息:

  • 在任意时刻,只有一个线程占用 CPU,处于运行状态
  • 多线程并发运行,实际上说的是多个线程轮流获取 CPU 的使用权
  • JVM 负责线程调度,按照特定机制为多个线程分配 CPU 使用权

线程调度模型

线程调度模型可以分为两类,分别是 分时调度模型 和 抢占式调度模型。

  • 分时调度模型:让所有线程轮流获取 CPU 的使用权,而且均分每个线程占用 CPU 的时间片,这种方式非常公平
  • 抢占式调度模型:JVM 使用的是抢占式调度模型,让优先级高的线程优先获取到 CPU 的使用权,如果在可运行池当中的线程优先级都一样,那就随机选取一个

Android 的线程调度

Android 的线程调度从两个因素决定,一个是 nice 值(即线程优先级),一个是 cgroup(即线程调度策略)。

对于 nice 值来说,它首先是在 Process 中定义的,值越小,进程优先级越高,默认值是 THREAD_PRIORITY_DEFAULT = 0,主线程的优先级也是这个值。修改 nice 值只需要在对应的线程下设置即可:

public class MyRunnable implements Runnable {<!-- -->
	@Override
	public void run() {<!-- -->
		Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT)
	}
}
// 附上 setThreadPriority() 文档说明
/**
 * Set the priority of the calling thread, based on Linux priorities.  See
 * {@link #setThreadPriority(int, int)} for more information.
 * 
 * @param priority A Linux priority level, from -20 for highest scheduling
 * priority to 19 for lowest scheduling priority.
 * 
 * @throws IllegalArgumentException Throws IllegalArgumentException if
 * &lt;var&gt;tid&lt;/var&gt; does not exist.
 * @throws SecurityException Throws SecurityException if your process does
 * not have permission to modify the given thread, or to use the given
 * priority.
 * 
 * @see #setThreadPriority(int, int)
 */
public static final native void setThreadPriority(int priority)
        throws IllegalArgumentException, SecurityException;

nice 值它还有其他的优先级可选:

public class Process {
    /**
     * Standard priority of application threads.
     * Use with {@link #setThreadPriority(int)} and
     * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
     * {@link java.lang.Thread} class.
     */
	public static final int THREAD_PRIORITY_DEFAULT = 0;
    /**
     * Lowest available thread priority.  Only for those who really, really
     * don't want to run if anything else is happening.
     * Use with {@link #setThreadPriority(int)} and
     * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
     * {@link java.lang.Thread} class.
     */
    public static final int THREAD_PRIORITY_LOWEST = 19;	
    /**
     * Standard priority background threads.  This gives your thread a slightly
     * lower than normal priority, so that it will have less chance of impacting
     * the responsiveness of the user interface.
     * Use with {@link #setThreadPriority(int)} and
     * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
     * {@link java.lang.Thread} class.
     */
    public static final int THREAD_PRIORITY_BACKGROUND = 10;    
    /**
     * Standard priority of threads that are currently running a user interface
     * that the user is interacting with.  Applications can not normally
     * change to this priority; the system will automatically adjust your
     * application threads as the user moves through the UI.
     * Use with {@link #setThreadPriority(int)} and
     * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
     * {@link java.lang.Thread} class.
     */
    public static final int THREAD_PRIORITY_FOREGROUND = -2;
    /**
     * Standard priority of system display threads, involved in updating
     * the user interface.  Applications can not
     * normally change to this priority.
     * Use with {@link #setThreadPriority(int)} and
     * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
     * {@link java.lang.Thread} class.
     */
    public static final int THREAD_PRIORITY_DISPLAY = -4;    
    /**
     * Standard priority of the most important display threads, for compositing
     * the screen and retrieving input events.  Applications can not normally
     * change to this priority.
     * Use with {@link #setThreadPriority(int)} and
     * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
     * {@link java.lang.Thread} class.
     */
    public static final int THREAD_PRIORITY_URGENT_DISPLAY = -8;
    /**
     * Standard priority of video threads.  Applications can not normally
     * change to this priority.
     * Use with {@link #setThreadPriority(int)} and
     * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
     * {@link java.lang.Thread} class.
     */
    public static final int THREAD_PRIORITY_VIDEO = -10;
    /**
     * Standard priority of audio threads.  Applications can not normally
     * change to this priority.
     * Use with {@link #setThreadPriority(int)} and
     * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
     * {@link java.lang.Thread} class.
     */
    public static final int THREAD_PRIORITY_AUDIO = -16;
    /**
     * Standard priority of the most important audio threads.
     * Applications can not normally change to this priority.
     * Use with {@link #setThreadPriority(int)} and
     * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
     * {@link java.lang.Thread} class.
     */
    public static final int THREAD_PRIORITY_URGENT_AUDIO = -19;
    /**
     * Minimum increment to make a priority more favorable.
     */
    public static final int THREAD_PRIORITY_MORE_FAVORABLE = -1;
    /**
     * Minimum increment to make a priority less favorable.
     */
    public static final int THREAD_PRIORITY_LESS_FAVORABLE =  1;    
}

在实践过程当中,如果只有 nice 值是不足够的。比如有一个 app 它有1个前台线程,而且它还有10个后台线程,虽然后台线程的优先级比较低,但是数量比较多,这10个后台线程对 CPU 的消耗量是可以影响到前台线程的性能的。所以 Android 需要一种机制来处理这种情况,也就是 cgroup。

Android 借鉴了 Linux 的 cgroup 来执行 更严格的前台和后台调度策略,后台优先级的线程会被隐式的移动到后台 group,而其他 group 的线程如果处于工作状态,那么后台这些线程它们将会被限制,只有很小的几率能够利用 CPU。

这种分离的调度策略既允许了后台线程来执行一些任务,同时又不会对用户可见的前台线程造成很大的影响,让前台线程有更多的 CPU。

或许你会有疑问:哪些线程会被移到后台 group?

  • 第一种就是那些 手动设置了优先级比较低的线程
  • 第二种就是 不在前台运行的那些应用程序的线程

线程调度小结

  • 线程过多会导致 CPU 频繁切换,降低线程运行效率。 在前面讲解启动优化的时候有强调要充足的利用线程比如异步启动任务,但是线程也不能无限制的使用
  • 正确认识任务重要性决定哪种优先级。 一般情况下线程工作量和优先级是成反比,比如线程的工作量越大,所做的工作没那么重要,那这个线程的优先级应该越低
  • 线程的优先级具有继承性。 比如在 A 线程创建了 B 线程,在我们没有指定线程优先级的情况下,B 线程的优先级是和 A 一样的。所以我们在 UI 线程中创建线程,线程的优先级是和 UI 线程一样的,这就会导致 UI 线程抢占 CPU 时间片的概率会变少

二、Android 异步方式汇总

Thread

使用 Thread 创建线程是最简单、常见的异步方式,但在实际项目中,它也就只有这个优点了,并不推荐直接使用 Thread 创建线程,主要有以下几点原因:

  • 不易复用,频繁创建及销毁开销大
  • 复杂场景不易使用

HandlerThread

是 Android 提供的一个自带消息循环的线程,它内部使用 串行的方式执行任务,比较 适合长时间运行,不断从队列中获取任务的场景。

IntentService

继承了 Android Service 组件,内部创建了 HandlerThread,相比 Service 是在主线程执行,IntentService 是 在子线程异步执行不占用主线程,而且 优先级比较高,不易被系统 kill。

AsyncTask

AsyncTask 是 Android 提供的工具类,内部的实现是使用了线程池,它比较大的好处是无需自己处理线程切换,但需要注意 AsyncTask 不同版本执行方式不一致的问题。

线程池

java 提供了线程池,在实际项目中比较推荐使用线程池的方式实现异步任务,它主要有以下优点:

  • 易复用,减少线程频繁创建、销毁的时间
  • 功能强大:定时、任务队列、并发数控制等,java 提供了 Executors 工具类可以很方便的创建一个线程池,也可以自己定制线程池

RxJava

RxJava 由强大的 Scheduler 集合提供,内部实际也是使用的线程池,它封装的非常完善,可以根据任务类型的不同指定使用不同的线程池,比如 IO 密集型的任务可以指定 Schedulers.IO,CPU 密集型任务可以指定 Schedulers.Computation

Single.just(xxx)
	.subscribeOn(Schedulers.IO) // 指定工作线程类型为 IO 密集型
	.observeOn(AndroidSchedulers.mainThread()) // 指定下游接收所在线程
	.subscribe();

三、Android线程优化实战

线程使用准则

  • 严禁使用直接new Thread()的方式
  • 提供基础线程池供各个业务线使用: 避免各个业务线各自维护一套线程池,导致线程数过多
  • 根据任务类型选择合适的异步方式: 比如优先级低且长时间执行可以使用Handler Thread,再比如:有一个任务需要定时执行,使用线程池更适合
  • 创建线程必须命名: 方便定位线程归属于哪一个业务方,在线程运行期可以使用Thread.currentThread().setName修改名字
  • 关键异步任务监控: 异步不等于不耗时,如果一个任务在主线程需要耗费500ms,那么它在异步任务中至少需要500ms,因为异步任务中优先级较低,耗费时间很可能会高于500ms,所以这里可以使用AOP的方式来做监控,并且结合所在的业务场景,根据监控结果来适时的做一些相对应的调整
  • 重视优先级设置: 使用Process.setThreadPriority();设置,并且可以设置多次

线程池优化实战

接下来针对线程池的使用来做一个简单的实践,还是打开我们之前的项目,这里说一下每次实践的代码都是基于第一篇启动优化的那个案例上写的。

首先新建一个包async,然后在包中创建一个类ThreadPoolUtils,这里我们创建可重用且固定线程数的线程池,核心数为5,并且对外暴露一个get方法,然后我们可以在任何地方都能获取到这个全局的线程池:

public class ThreadPoolUtils {
    //创建定长线程池,核心数为5
    private static ExecutorService mService = Executors.newFixedThreadPool(5, new ThreadFactory() {
        @Override
        public Thread newThread(Runnable runnable) {
            Thread thread = new Thread(runnable,"ThreadPoolUtils");//设置线程名
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); //设置线程优先级
            return thread;
        }
    });
    //获取全局的线程池
    public static ExecutorService getService(){
        return mService;
    }
}

然后使用的时候就可以在你需要的地方直接调用了,并且你在使用的时候还可以修改线程的优先级以及线程名称:

        //使用全局统一的线程池
        ThreadPoolUtils.getService().execute(new Runnable() {
            @Override
            public void run() {
                Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT); //修改线程优先级
                String oldName = Thread.currentThread().getName();
                Thread.currentThread().setName("Jarchie"); //修改线程名称
                Log.i("MainActivity","");
                Thread.currentThread().setName(oldName); //将原有名称改回去
            }
        });

四、定位线程创建者

如何确定线程创建者

当你的项目做的越来越大的时候一般情况下线程都会变的非常多,最好是能够对整体的线程数进行收敛,那么问题来了,如何知道某个线程是在哪里创建的呢?不仅仅是你自己的项目源码,你依赖的第三方库、aar中都有线程的创建,如果单靠人眼review代码的方式,工作量很大而且你还不一定能找的全。

并且你这次优化完了线程数,你还要考虑其他人新加的线程是否合理,所以就需要能够建立一套很好的监控预防手段。然后针对这些情况来做一个解决方案的总结分析,主要思想就是以下两点:

  • 创建线程的位置获取堆栈
  • 所有的异步方式,都会走到new Thread

解决方案:

  • 特别适合Hook手段
  • 找Hook点:构造函数或者特定方法
  • Thread的构造函数

可以在构造函数中加上自己的逻辑,获取当前的调用栈信息,拿到调用栈信息之后,就可以分析看出某个线程是否使用的是统一的线程池,也可以知道某个线程具体属于哪个业务方。

Epic实战

Epic简介

  • Epic是一个虚拟机层面、以Java Method为粒度的运行时Hook框架
  • 支持Android4.0-10.0(我的手机上程序出现了闪退,后来查找原因发现这个库开源版本一些高版本手机好像不支持)

Epic使用

  • implementation 'me.weishu:epic:0.6.0'
  • 继承XC_MethodHook,实现相应逻辑
  • 注入Hook:DexposedBridge.findAndHookMethod

代码中使用

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //Hook Thread类的构造函数,两个参数:需要Hook的类,MethodHook的回调
        DexposedBridge.hookAllConstructors(Thread.class, new XC_MethodHook() {
            //afterHookedMethod是Hook此方法之后给我们的回调
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                super.afterHookedMethod(param); //Hook完成之后会回调到这里
                //实现自己的逻辑,param.thisObject可以拿到线程对象
                Thread thread = (Thread) param.thisObject;
                //Log.getStackTraceString打印当前的调用栈信息
                Log.i(thread.getName()   "stack", Log.getStackTraceString(new Throwable()));
            }
        });
    }

如果你的手机支持的话,这个时候运行程序应该就可以看到线程打印出来的堆栈信息了

五、优雅实现线程收敛

线程收敛常规方案

  • 根据线程创建堆栈考量合理性,使用统一线程库
  • 各业务线需要移除自己的线程库使用统一的线程库

基础库如何使用线程

  • 直接依赖线程库
  • 缺点:线程库更新可能会导致基础库也跟着更新

基础库优雅使用线程

  • 基础库内部暴露API:setExecutor
  • 初始化的时候注入统一的线程库

举个栗子:

比如这里有一个日志工具类,我们将它作为应用的日志基础库,假设它内部有一些异步操作,原始的情况下是它自己内部实现的,然后现在在它内部对外暴露一个API,如果外部注入了一个ExecutorService,那么我们就使用外部注入的这个,如果外部没有注入,那就使用它默认的,代码如下所示:

public class LogUtils {
    private static ExecutorService mExecutorService;
    public static void setExecutor(ExecutorService executorService){
        mExecutorService = executorService;
    }
    public static final String TAG = "Jarchie";
    public static void i(String msg){
        if(Utils.isMainProcess(BaseApp.getApplication())){
            Log.i(TAG,msg);
        }
        // 异步操作
        if(mExecutorService != null){
            mExecutorService.execute(() -> {
                ...
            });
        }else {
            //使用原有的
            ...
        }
    }
}

统一线程库

  • 区分任务类型:IO密集型、CPU密集型
  • IO密集型任务不消耗CPU,核心池可以很大(网络请求、IO读写等)
  • CPU密集型任务:核心池大小和CPU核心数相关(如果并发数超过核心数会导致CPU频繁切换,降低执行效率)

举个栗子:根据上面的说明,可以做如下的设置:

    //获取CPU的核心数
    private int CPUCOUNT = Runtime.getRuntime().availableProcessors();
    //cpu线程池,核心数大小需要和cpu核心数相关联,这里简单的将它们保持一致了
    private ThreadPoolExecutor cpuExecutor = new ThreadPoolExecutor(CPUCOUNT, CPUCOUNT,
            30, TimeUnit.SECONDS, new LinkedBlockingDeque<>(), sThreadFactory);
    //IO线程池,核心数64,这个数量可以针对自身项目再确定
    private ThreadPoolExecutor iOExecutor = new ThreadPoolExecutor(64, 64,
            30, TimeUnit.SECONDS, new LinkedBlockingDeque<>(), sThreadFactory);
    //这里面使用了一个count作为标记
    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);
        public Thread newThread(Runnable runnable) {
            return new Thread(runnable, "ThreadPoolUtils #"   mCount.getAndIncrement());
        }
    };

然后在你实际项目中需要区分具体的任务类型,针对性的选择相应的线程池进行使用。 以上就是对于Android线程优化方面的总结了,今天的内容还好不算多,觉得有用的朋友可以看看。

以上就是Android 线程优化知识点学习的详细内容,更多关于Android 线程优化的资料请关注Devmax其它相关文章!

Android 线程优化知识点学习的更多相关文章

  1. html5 canvas合成海报所遇问题及解决方案总结

    这篇文章主要介绍了html5 canvas合成海报所遇问题及解决方案总结,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. Html5 video标签视频的最佳实践

    这篇文章主要介绍了Html5 video标签视频的最佳实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  3. HTML5在微信内置浏览器下右上角菜单的调整字体导致页面显示错乱的问题

    HTML5在微信内置浏览器下,在右上角菜单的调整字体导致页面显示错乱的问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧

  4. ios – containerURLForSecurityApplicationGroupIdentifier:在iPhone和Watch模拟器上给出不同的结果

    我使用默认的XCode模板创建了一个WatchKit应用程序.我向iOSTarget,WatchkitAppTarget和WatchkitAppExtensionTarget添加了应用程序组权利.(这是应用程序组名称:group.com.lombax.fiveminutes)然后,我尝试使用iOSApp和WatchKitExtension访问共享文件夹URL:延期:iOS应用:但是,测试NSURL

  5. iOS:核心图像和多线程应用程序

    我试图以最有效的方式运行一些核心图像过滤器.试图避免内存警告和崩溃,这是我在渲染大图像时得到的.我正在看Apple的核心图像编程指南.关于多线程,它说:“每个线程必须创建自己的CIFilter对象.否则,你的应用程序可能会出现意外行为.”这是什么意思?我实际上是试图在后台线程上运行我的过滤器,所以我可以在主线程上运行HUD(见下文).这在coreImage的上下文中是否有意义?

  6. ios – 多个NSPersistentStoreCoordinator实例可以连接到同一个底层SQLite持久性存储吗?

    我读过的关于在多个线程上使用CoreData的所有内容都讨论了使用共享单个NSPersistentStoreCoordinator的多个NSManagedobjectContext实例.这是理解的,我已经使它在一个应用程序中工作,该应用程序在主线程上使用CoreData来支持UI,并且具有可能需要一段时间才能运行的后台获取操作.问题是NSPersistentStoreCoordinator会对基础

  7. ios – XCode断点应该只挂起当前线程

    我需要调试多线程错误.因此,为了获得生成崩溃的条件,我需要在代码中的特定点停止一个线程,并等待另一个线程到达第二个断点.我现在遇到的问题是,如果一个线程遇到断点,则所有其他线程都被挂起.有没有办法只停止一个线程,让其他线程运行,直到它们到达第二个断点?)其他更有趣的选择:当你点击第一个断点时,你可以进入控制台并写入这应该在该断点处暂停当前上下文中的线程一小时.然后在Xcode中恢复执行.

  8. ios – 在后台线程中写入Realm后,主线程看不到更新的数据

    >清除数据库.>进行API调用以获取新数据.>将从API检索到的数据写入后台线程中的数据库中.>从主线程上的数据库中读取数据并渲染UI.在步骤4中,数据应该是最新数据,但我们没有看到任何数据.解决方法具有runloops的线程上的Realm实例,例如主线程,updatetothelatestversionofthedataintheRealmfile,因为通知被发布到其线程的runloop.在后台

  9. Ionic – Splash Screen适用于iOS,但不适用于Android

    我有一个离子应用程序,其中使用CLI命令离子资源生成的启动画面和图标iOS版本与正在渲染的启动画面完美配合,但在Android版本中,只有在加载应用程序时才会显示白屏.我检查了config.xml文件,所有路径看起来都是正确的,生成的图像出现在相应的文件夹中.(我使用了splash.psd模板来生成它们.我错过了什么?这是config.xml文件供参考,我觉得我在这里做错了–解决方法在config.xml中添加以下键:它对我有用!

  10. ios – 无法启动iPhone模拟器

    /Library/Developer/CoreSimulator/Devices/530A44CB-5978-4926-9E91-E9DBD5BFB105/data/Containers/Bundle/Application/07612A5C-659D-4C04-ACD3-D211D2830E17/ProductName.app/ProductName然后,如果您在Xcode构建设置中选择标准体系结构并再次构建和运行,则会产生以下结果:dyld:lazysymbolbindingFailed:Symbol

随机推荐

  1. Flutter 网络请求框架封装详解

    这篇文章主要介绍了Flutter 网络请求框架封装详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. Android单选按钮RadioButton的使用详解

    今天小编就为大家分享一篇关于Android单选按钮RadioButton的使用详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧

  3. 解决android studio 打包发现generate signed apk 消失不见问题

    这篇文章主要介绍了解决android studio 打包发现generate signed apk 消失不见问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

  4. Android 实现自定义圆形listview功能的实例代码

    这篇文章主要介绍了Android 实现自定义圆形listview功能的实例代码,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  5. 详解Android studio 动态fragment的用法

    这篇文章主要介绍了Android studio 动态fragment的用法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  6. Android用RecyclerView实现图标拖拽排序以及增删管理

    这篇文章主要介绍了Android用RecyclerView实现图标拖拽排序以及增删管理的方法,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下

  7. Android notifyDataSetChanged() 动态更新ListView案例详解

    这篇文章主要介绍了Android notifyDataSetChanged() 动态更新ListView案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下

  8. Android自定义View实现弹幕效果

    这篇文章主要为大家详细介绍了Android自定义View实现弹幕效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  9. Android自定义View实现跟随手指移动

    这篇文章主要为大家详细介绍了Android自定义View实现跟随手指移动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. Android实现多点触摸操作

    这篇文章主要介绍了Android实现多点触摸操作,实现图片的放大、缩小和旋转等处理,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部