Android多线程机制简介

当android应用跑起来的时候,我们能够看到应用上流畅的界面就是因为主线程在一直死循环的结果,界面在主线程不断刷新,所以主线程不适合一些耗时的IO操作,会造成界面卡顿。顺带提一句android的ANR是因为主线程在某一次循环的时候某一处代码太耗时间,或者说某一处死循环了导致的 一些IO操作我们总是会放到后台线程里面去执行,提一下,Handler可不是一个后台线程,post一个耗时任务还是会阻塞主线程,他只是一个线程切换的工具,后台线程就是一个Thread,在android里面,一般会使用HandlerThread。而且这个线程需要自己手动去退出,他自己是不会退出的。看以下代码:

java 复制代码
public static void main(String[] args) {
    CustomThread mainThread = new CustomThread();
    mainThread.start();
}
public class CustosmThread extends Thread {
​
    @Override
    public void run() {
        while (true) {
        }
    }
}

上面的代码启动了一个线程,是一个永不结束的线程。然后继续改造,我希望能够往里面不定期添加一些任可执行任务,变成

java 复制代码
public class CustomThread extends Thread {
    private Runnable task;
​
    public void setTask(Runnable task) {
        this.task = task;
    }
​
    @Override
    public void run() {
        while (true) {
            // draw
            if (task != null) {
                task.run();
                task = null;
            }
        }
    }
}
​
public static void main(String[] args) {
    CustomThread mainThread = new CustomThread();
    mainThread.start();
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    mainThread.setTask(() -> System.out.println(123));
}

假设主线程在3秒钟之后往里面添加一个任务,这里需要注意task又被多线程访问了,所以需要锁

java 复制代码
public class CustomThread extends Thread {
    private Runnable task;
​
    public synchronized void setTask(Runnable task) {
        this.task = task;
    }
​
    @Override
    public void run() {
        while (true) {
            // 这里锁不能加在方法上,不然会导致主线程无法访问setTask()
            synchronized (this) {
                if (task != null) {
                    task.run();
                    task = null;
                }
            }
        }
    }
}

这样主线程就能在特定时间往里面添加任务了。然后我还希望这个线程能够退出,继续改造。

java 复制代码
public class CustomThread extends Thread {
    private Runnable task;
    // 还是需要注意同步性
    private final AtomicBoolean quit = new AtomicBoolean(false);
​
    public synchronized void setTask(Runnable task) {
        this.task = task;
    }
​
    public void quitThread() {
        this.quit.set(true);
    }
​
    @Override
    public void run() {
        while (quit.get()) {
            synchronized (this) {
                if (task != null) {
                    task.run();
                    task = null;
                }
            }
        }
    }
}

这样主线程就能在特定时机退出这个线程了,这差不多就是HandlerThread的雏形。 只不过android里面还会再细分,HandlerThread其实是一个套壳类,里面的功能会被再次抽取成一个Looper类,Looper类里面会再抽取循环归循环,消息处理归消息处理等等,不展开说了......

再讲一下ThreadLocal,是各个线程自己各有的一个变量,相互之间必定不共享。这个和之前的线程数据同步刚好是反着来的,每个线程都有一块自己的内存,所以对于成员变量的修改需要同步给各个线程,volatile的作用就是这个。

java 复制代码
public static void main(String[] args) {
    final ThreadLocal<Integer> integerThreadLocal = new ThreadLocal<>();
    new Thread(() -> {
        integerThreadLocal.set(1);
        // do something
        integerThreadLocal.get(); // 肯定是1
    }).start();
    new Thread(() -> {
        integerThreadLocal.set(1);
        // do something
        integerThreadLocal.get(); // 肯定是2
    }).start();
}

这里不管线程顺序怎么执行,上面那个线程获取的值肯定是1,下面那个获取的肯定是2。 说一下这东西在android里面哪里出现了 有时候我们需要判断当前线程是不是主线程,可以用Looper.getMainLooper() == Looper.myLooper()进行判断,去看下源码,mainLooper()和myLooper()本质上差不多,去看myLooper()

java 复制代码
public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

而这个sThreadLocal定义是static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();,由此可以知道,不同的线程返回的是不同的Looper,因为ThreadLocal里面保存的变量就是每个线程各一份,且互不相关。至于这个变量在哪里初始化的......去看源码吧,可以想想线程启动的时候什么方法最先调用,肯定是run()方法,所以在ThreadLocal的run方法里面追踪。 上面说了android里面Handler只是一个线程切换的工具,不是说在主线程post一个耗时任务这样也可以。当你new一个Handler的时候,当前是在哪个线程,post的任务就会在哪个线程执行。 我们可以选择在主线程中new Thread(),让线程去处理耗时任务,等任务处理好了之后,再通过主线程的Handler post出去,也可以创建一个HandlerThread对象,用他的Looper创建一个Handler对象,所以Handler有个构造方法是可以传入Looper对象的

java 复制代码
public Handler(@NonNull Looper looper) {
    this(looper, null, false);
}

这个Handler是可以直接post一个耗时任务的,但需要注意的是,这个Handler就不能处理UI更新了,因为已经在子线程中了。

讲了这么多,来点实际的,android中多线程应用的场景就是切换线程,主线程需要子线程去处理一些耗时操作,之后把结果传回主线程更新UI显示。 举个栗子:UI上点击一个按钮,发起网络请求,处理请求结果并显示在界面上 先来一个UI,以下代码用Kotlin写了,用Java写累死了

kotlin 复制代码
class ThreadDemo : Fragment() {
    private val text by lazy {
        AppCompatTextView(requireContext()).apply {
            layoutParams = ViewGroup.LayoutParams(-1, -1)
        }
    }
    private val button by lazy {
        AppCompatButton(requireContext()).apply {
            val params = FrameLayout.LayoutParams(-2, -2)
            params.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
            layoutParams = params
            text = "请求"
            setOnClickListener { requestHttp() }
        }
    }
    private val threadView by lazy {
        FrameLayout(requireContext()).apply {
            addView(text)
            addView(button)
        }
    }
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return threadView
    }
​
    private fun requestHttp() {}
}

UI很简单,界面上就是一个全屏的TextView,底部居中一个自适应大小的Button,点击触发requestHttp()函数,这个函数要做事情就是发起请求,处理结果,更新UI,为了简单说明,这里不考虑异常情况,不考虑取消的情况。

方法一:Handler,(在我看来Runnable,HandlerThread和这种方式没本质区别,没有线程管理,需要手动去切换线程)

kotlin 复制代码
private val handler = Handler()
private fun requestHttp() {
    thread {
        val client = OkHttpClient()
        val request = Request.Builder().url("").get().build()
        val call = client.newCall(request)
        call.enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                handler.post { text.text = "error" }
            }
​
            override fun onResponse(call: Call, response: Response) {
                handler.post { text.text = response.body?.string() }
            }
        })
    }
}

方法二:AsyncTask(通过线程池来管理线程,能够自动在对应的地方切换线程,用的是Handler) 个人觉得没有什么用,被时代淘汰了,官方都抛弃了,凉透了! 算了,还是讲讲吧。

kotlin 复制代码
inner class MyTask : AsyncTask<Unit, Unit, String>() {
    // 没啥可以准备的,过
    override fun onPreExecute() = Unit
    // 没有进度条,过
    override fun onProgressUpdate(vararg values: Unit?) = Unit
    // 不考虑取消,过
    override fun onCancelled() = Unit
    override fun doInBackground(vararg params: Unit?): String {
        return try {
                val client = OkHttpClient()
                val request = Request.Builder().url("").get().build()
                val call = client.newCall(request)
                call.execute().body?.string()!!
            } catch (exception: Exception) {
                "error"
        }
    }
    override fun onPostExecute(result: String?) {
        text.text = result
    }
}
​
private fun requestHttp() {
    val task = MyTask()
    task.execute(Unit)
}

上面的代码中,不需要手动切换线程,各个方法都在各自对应的线程里,感觉逻辑都在一个类里面,比较集中了,但我个人感觉还是不好,是真的啰嗦。除了doInBackground是子线程,其他都是在主线程虽然是这个也和创建AsyncTask时的Looper有关 提一下,inner在kotlin表示这是一个内部类,类似于Java里面不加static的内部类,所以onPostExecute里面可以访问fragment的text,这个还有一个臭名昭著的内存泄漏的说法,是不是真的会内存泄漏(泄漏了严不严重)很多人其实并不清楚,只是感觉大家都这么说,那我也就这么干,然后就加上对外的弱引用。具体证明不在这展开...

方法三:RxJava,可以管理线程,无需手动切换,可以实现多个请求并发,并且合并结果

kotlin 复制代码
private fun requestHttp() {
    Observable.create<String> {
        val client = OkHttpClient()
        val request = Request.Builder().url("").get().build()
        val call = client.newCall(request)
        call.enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                it.onError(Exception("error"))
            }
​
            override fun onResponse(call: Call, response: Response) {
                val result = response.body?.string() ?: "error"
                it.onNext(result)
                it.onComplete()
            }
        })
    }
        .observeOn(AndroidSchedulers.mainThread())
        .subscribeOn(Schedulers.io())
        .doOnError {
            text.text = it.message
        }.subscribe {
            text.text = it
        }
}

可以看到,终于是一个函数就能搞定的事情了,不用在外面定义什么Handler,什么AsyncTask,android studio也没有什么内存泄漏的警告真正会不会内存泄漏其实是看个人考虑是否周全 当然,实际在使用的时候也不会是直接的使用Observable,而且需要考虑取消的情况

方法四:kotlin协程严重夹带私人感情,强烈推荐RxJava能做的全部都行,并且能以同步编码方式书写异步代码,阅读性更强。 先想想,不考虑其他情况,让一个不懂程序的人来思考这样的实现,用自然语言描述就是:

kotlin 复制代码
private fun requestHttp() {
    // 从网络请求那里获取需要的数据,不管成功还是失败都会返回一个数据
    // 对数据进行处理
    // 把结果显示在UI上
}

但是在android中需要考虑主线程子线程切换问题,而且网络请求需要客户端去触发,所以按照上面的步骤可以得到以下代码

kotlin 复制代码
private fun requestHttp() = GlobalScope.launch(Dispatchers.Main) {
    // 从网络请求那里获取需要的数据
    val result = withContext(Dispatchers.IO) {
        try {
            val client = OkHttpClient()
            val request = Request.Builder().url("").get().build()
            val call = client.newCall(request)
            // 成功的结果
            call.execute().body?.string()
        } catch (exception: Exception) {
            // 失败的结果
            "error"
        }
    }
    // 对数据进行处理
    println(result)
    // 把结果显示在UI上
    text.text = result
}

代码顺序执行,一气呵成,没有回调,干净清爽。用回调的那种方式需要考虑成功时怎样?失败时怎样?调用回调本身异常时又需要try-catch,很复杂很麻烦,处理后续步骤会形成多个分支,而用协程就可以做到顺序执行,一条线。

相关推荐
星释4 小时前
二级等保实战:MySQL安全加固
android·mysql·安全
沐怡旸8 小时前
【底层机制】垃圾回收(GC)底层原理深度解析
android·面试
whatever who cares9 小时前
android/java中gson的用法
android·java·开发语言
用户0273851840269 小时前
【Android】活动的正/异常生命周期和启动模式、标志位详解
android
nono牛10 小时前
MTK平台详解`adb devices`输出的序列号组成
android·linux·adb·智能手机
zhangphil11 小时前
Android通过SQL查询trace分析进程启动线程总数量
android
下位子11 小时前
『OpenGL学习滤镜相机』- Day3: 着色器基础 - GLSL 语言
android·opengl
bqliang11 小时前
Jetpack Navigation 3:领航未来
android·android studio·android jetpack
云存储小天使11 小时前
安卓蛙、苹果蛙为什么难互通?
android