使用mitmproxy + python做拦截代理

本文是一个较为完整的 mitmproxy 教程,侧重于介绍如何开发拦截脚本,帮助读者能够快速得到一个自定义的代理工具。

本文假设读者有基本的 python 知识,且已经安装好了一个 python 3 开发环境。如果你对 nodejs 的熟悉程度大于对 python,可移步到 anyproxy,anyproxy 的功能与 mitmproxy 基本一致,但使用 js 编写定制脚本。除此之外我就不知道有什么其他类似的工具了,如果你知道,欢迎评论告诉我。

本文基于 mitmproxy v4,当前版本号为 v4.0.1

继续阅读使用mitmproxy + python做拦截代理

Cannot resolve symbol KeyEventCompat(android.support.v4.view.KeyEventCompat找不到)

今天我把support版本升到了28.0.0  发现V4包  下的KeyEventCompat 类找不到了

com.android.support:appcompat-v4:28.0.0

那是因为KeyEventCompat类被取消了 hasNoModifiers() 方法已经被KeyEvent实现了

修改为:

if (event.hasNoModifiers()) {
      handled = arrowScroll(FOCUS_FORWARD);
} else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
       handled = arrowScroll(FOCUS_BACKWARD);
}

参考链接


Android Q(Android 10)适配

应用读取 Device_ID

Android Q 之前有如下代码,获取设备Id,IMEI等

TelephonyManager telManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
telManager.getDeviceId();
telManager.getImei();

添加下面权限,并且需要动态申请权限

<uses-permission android:name="android.permission.READ_PHONE_STATE" />

在 Android Q 上调用上面方法会报错

java.lang.SecurityException: getDeviceId: The user xxxx does not meet the requirements to access device identifiers.

或者

E/Exception: getSubscriberId: The user xxxx does not meet the requirements to access device identifiers.
    java.lang.SecurityException: getSubscriberId: The user xxxx does not meet the requirements to access device identifiers.

在 Android Q 上上面方法已经不能使用了,如果获取设备唯一Id,需要使用其他方式了,谷歌提供的获取唯一标识符做法见 文档,也可以用Android_ID,上面这些也不是绝对能得到一个永远不变的Id,可能需要多种方案获取其他Id,比如有谷歌商店的手机可以使用谷歌提供的广告Id,还有其他厂商一般都会提供手机的一个唯一Id。

我们项目现在使用下面这种方式 参考链接。要求生成之后,存储在应用的本地数据库中,读取的时候,优先从本地数据库读取上次存储的设备ID,不要每次都调用下面的接口获取,下面的接口不能保证每次都返回相同结果。

public static String getDeviceId(Context context) {
    final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
    if (targetSdkVersion > Build.VERSION_CODES.P && Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
        return getUniqueID(context);
    } else {
        return getTelId(context);
    }
}

private static String getTelId(Context context) {
    final TelephonyManager manager = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
    return manager.getDeviceId();
}

private static String getUniqueID(Context context) {
    String id = null;
    final String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
    if (!TextUtils.isEmpty(androidId) && !"9774d56d682e549c".equals(androidId)) {
        try {
            UUID uuid = UUID.nameUUIDFromBytes(androidId.getBytes("utf8"));
            id = uuid.toString();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    if (TextUtils.isEmpty(id)) {
        id = getUUID();
    }

    return TextUtils.isEmpty(id) ? UUID.randomUUID().toString() : id;
}

private static String getUUID() {
    String serial = null;

    String m_szDevIDShort = "35" +
            Build.BOARD.length() % 10 + Build.BRAND.length() % 10 +

            ((null != Build.CPU_ABI) ? Build.CPU_ABI.length() : 0) % 10 + 
            
            Build.DEVICE.length() % 10 + Build.DISPLAY.length() % 10 + 
            
            Build.HOST.length() % 10 + Build.ID.length() % 10 + 
            
            Build.MANUFACTURER.length() % 10 + Build.MODEL.length() % 10 + 
            
            Build.PRODUCT.length() % 10 + Build.TAGS.length() % 10 + 
            
            Build.TYPE.length() % 10 + Build.USER.length() % 10; //13 位

    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                serial = android.os.Build.getSerial();
            } else {
                serial = Build.SERIAL;
            }
            //API>=9 使用serial号
            return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
        } catch (Exception exception) {
            serial = "serial" + UUID.randomUUID().toString(); // 随便一个初始化
        }
    } else {
        serial = android.os.Build.UNKNOWN + UUID.randomUUID().toString(); // 随便一个初始化
    }

    //使用硬件信息拼凑出来的15位号码
    return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
}

 

参考链接


error=86, Bad CPU type in executable

最近在维护一个`N`久的项目时,发现在`mac`升级系统为`10.15.5`后(Android Studio 4.0,gradle 2.3.1),编译失败了,报错如下:

Cannot run program "/Users/xxxx/Android/sdk/build-tools/23.0.1/aapt": error=86, Bad CPU type in executable

原因是最新版本的`macOS Catalina(10.15.5)`已经不支持`32`位的应用了,只能运行`64`位的应用。

解决方法:升级工程的`buildToolsVersion`,本例中将`23.0.1` 升级成`25.0.3`。

参考链接


error=86, Bad CPU type in executable

Android如何实现exclude aar包中的某个jar包

要移除的jar包不在aar包中的classes.jar中

直接把aar包里的Jar打包的时候给去掉,就像下面这这样。注意,要使用exclude module这种方式,直接使用exclude group方式没有效果。exclude group的方法适用于exclude JAR包中的文件。

implementation(name: '×××××××aar', ext: 'aar') {
    exclude module: 'gson'
}

但是,上面的操作有时候不能生效,尤其是复杂依赖的情况下。我们可能需要在每个间接依赖上都手工进行排除操作,这样就非常麻烦了。

这时候最简单的办法就是移除不需要的jar包,然后重新打包了。

或者要移除的jar包在aar包中的classes.jar中

这个时候,使用exclude方法已经不能生效了。你可以使用下面的通用方法

解压aar文件到tmpDir目录下

$ unzip ×××.aar -d tmpDir

找到classes.jar包,用压缩工具打开,删除目标文件

将tmpDir重新打包成一个新的aar

$ jar cvf ×××NewLib.aar -C tmpDir/ .

另外,有人开发了 排除AAR(Jar)包中冗余或者冲突类的gradle脚本 。如果图方便的话,可以直接用这个脚本来排除。

参考链接


Android ViewPager存在界面卡住的BUG

最近开发的项目出现界面莫名其秒的卡住,直到发生`ANR`异常退出的问题。
问题排查许久,才发现是由于使用的`ViewPager`导致的。
在`ViewPager`中使用`setCurrentItem`长距离设置当前显示的位置的时候,比如`0->600`,`600->1000`,`1000->500` 这样的长距离跳转。会导致函数卡住在

void populate(int newCurrentItem)

函数中很长时间, 甚至一直不能跳出循环。具体卡住的的循环代码如下:

for(int pos = this.mCurItem + 1; pos < N; ++pos) { 
    if (extraWidthRight >= rightWidthNeeded && pos > endPos) { 
        if (ii == null) {
            break;
        }

        if (pos == ii.position && !ii.scrolling) {
            this.mItems.remove(itemIndex);
            this.mAdapter.destroyItem(this, pos, ii.object);
            ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
        }
    } else if (ii != null && pos == ii.position) {
        extraWidthRight += ii.widthFactor;
        ++itemIndex;
        ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
    } else {
        ii = this.addNewItem(pos, itemIndex);
        ++itemIndex;
        extraWidthRight += ii.widthFactor;
        ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
    }
}

上述代码中循环体会一直循环到`N`结束为止。而这个`N`是通过

int N = this.mAdapter.getCount();

赋值的。恰好,我们的代码中返回的是`Integer.MAX_VALUE`。咨询了同事,当时设置这个数值的目的是为了解决循环滚动展示才设置的。代码是直接抄的 Android无限广告轮播 自定义BannerView / 如何优雅的实现一个可复用的 PagerAdapter

通过Google搜索关键词 `BannerAdapter extends PagerAdapter` 可以找到类似的代码。

具体的复现`BUG`的例子代码如下:

package com.mobibrw.viewpager;

import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;

import java.util.Random;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final ViewPager viewPager = findViewById(R.id.viewPager);
        viewPager.setAdapter(new PagerAdapter() {
            @Override
            public int getCount() {
                return Integer.MAX_VALUE;
            }

            @Override
            public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
                return view == object;
            }

            @Override
            public void destroyItem(ViewGroup container, int position, Object object) {
                container.removeView((View) object);
            }

            @Override
            public Object instantiateItem(ViewGroup container, final int position) {
                final View view = LayoutInflater.from(MainActivity.this).inflate(R.layout.pager_view, null);
                final TextView tv = view.findViewById(R.id.text);
                tv.setText("" + position);
                container.addView(view);
                return view;
            }
        });

        final Button btnCmd = findViewById(R.id.btnCmd);
        btnCmd.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Random rdm = new Random(System.currentTimeMillis());
                int rd = Math.abs(rdm.nextInt())%700 + 1;
                viewPager.setCurrentItem(rd);
            }
        });
    }
}
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <android.support.v4.view.ViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipChildren="false"
        android:clipToPadding="false"/>
    <Button
        android:id="@+id/btnCmd"
        android:text="@string/change_size"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        />

</android.support.constraint.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.53" />

</android.support.constraint.ConstraintLayout>

完整的项目代码可以点击此处下载 ViewPager / ViewPagerAppCompatV7。项目编译完成之后,多点击几次 `Change Size` 按钮就可以复现界面长时间卡住的情况。第二个项目是支持`appcompat-v7:18.0.0`的版本,目的是观察各个版本的代码是否存在不同,结论是,都一样

这个问题目前的解决方法就是保证`ViewPager`中的数据不要太多,比如底层分页显示,每页不要太多。另外就是不要大距离跳转,否则会有很多问题

如果能把`android.support`升级到`androidx`的话,可以试试最新的`ViewPager2`,最新的`ViewPager2`是用`RecyclerView `实现的,应该不会有以前的问题了。

这个问题,我已经向Google提交了BUG,估计最后的答复也是要升级到`androidx`。

参考链接


Android无限广告轮播 - ViewPager源码分析

接口幂等性这么重要,它是什么?怎么实现?

什么是幂等性?

对于同一笔业务操作,不管调用多少次,得到的结果都是一样的。

幂等性设计

我们以对接支付宝充值为例,来分析支付回调接口如何设计?

如果我们系统中对接过支付宝充值功能的,我们需要给支付宝提供一个回调接口,支付宝回调信息中会携带(out_trade_no【商户订单号】,trade_no【支付宝交易号】),trade_no在支付宝中是唯一的,out_trade_no在商户系统中是唯一的。

回调接口实现有以下实现方式。

方式1(普通方式)

过程如下:

1.接收到支付宝支付成功请求
2.根据trade_no查询当前订单是否处理过
3.如果订单已处理直接返回,若未处理,继续向下执行
4.开启本地事务
5.本地系统给用户加钱
6.将订单状态置为成功
7.提交本地事务

上面的过程,对于同一笔订单,如果支付宝同时通知多次,会出现什么问题?当多次通知同时到达第2步时候,查询订单都是未处理的,会继续向下执行,最终本地会给用户加两次钱。

此方式适用于单机其,通知按顺序执行的情况,只能用于自己写着玩玩。

方式2(jvm加锁方式)

方式1中由于并发出现了问题,此时我们使用java中的Lock加锁,来防止并发操作,过程如下:

1.接收到支付宝支付成功请求
2.调用java中的Lock加锁
3.根据trade_no查询当前订单是否处理过
4.如果订单已处理直接返回,若未处理,继续向下执行
5.开启本地事务
6.本地系统给用户加钱
7.将订单状态置为成功
8.提交本地事务
9.释放Lock锁

分析问题:
Lock只能在一个jvm中起效,如果多个请求都被同一套系统处理,上面这种使用Lock的方式是没有问题的,不过互联网系统中,多数是采用集群方式部署系统,同一套代码后面会部署多套,如果支付宝同时发来多个通知经过负载均衡转发到不同的机器,上面的锁就不起效了。此时对于多个请求相当于无锁处理了,又会出现方式1中的结果。此时我们需要分布式锁来做处理。

方式3(悲观锁方式)

使用数据库中悲观锁实现。悲观锁类似于方式二中的Lock,只不过是依靠数据库来实现的。数据中悲观锁使用for update来实现,过程如下:

1.接收到支付宝支付成功请求
2.打开本地事物
3.查询订单信息并加悲观锁

select * from t_order where order_id = trade_no for update;

4.判断订单是已处理
5.如果订单已处理直接返回,若未处理,继续向下执行
6.给本地系统给用户加钱
7.将订单状态置为成功
8.提交本地事物

重点在于for update,对for update,做一下说明:
1.当线程A执行for update,数据会对当前记录加锁,其他线程执行到此行代码的时候,会等待线程A释放锁之后,才可以获取锁,继续后续操作。
2.事物提交时,for update获取的锁会自动释放。

方式3可以正常实现我们需要的效果,能保证接口的幂等性,不过存在一些缺点:
1.如果业务处理比较耗时,并发情况下,后面线程会长期处于等待状态,占用了很多线程,让这些线程处于无效等待状态,我们的web服务中的线程数量一般都是有限的,如果大量线程由于获取for update锁处于等待状态,不利于系统并发操作。

方式4(乐观锁方式)

依靠数据库中的乐观锁来实现。

1.接收到支付宝支付成功请求
2.查询订单信息

select * from t_order where order_id = trade_no;

3.判断订单是已处理
4.如果订单已处理直接返回,若未处理,继续向下执行
5.打开本地事物
6.给本地系统给用户加钱
7.将订单状态置为成功,注意这块是重点,伪代码:

update t_order set status = 1 where order_id = trade_no where status = 0;
//上面的update操作会返回影响的行数num
if(num==1){
 //表示更新成功
 提交事务;
}else{
 //表示更新失败
 回滚事务;
}

注意:
update t_order set status = 1 where order_id = trade_no where status = 0; 是依靠乐观锁来实现的,status=0作为条件去更新,类似于java中的cas操作;关于什么是cas操作,可以移步:什么是 CAS 机制 ( http://www.itsoku.com/article/63 )?
执行这条sql的时候,如果有多个线程同时到达这条代码,数据内部会保证update同一条记录会排队执行,最终最有一条update会执行成功,其他未成功的,他们的num为0,然后根据num来进行提交或者回滚操作。

方式5(唯一约束方式)

依赖数据库中唯一约束来实现。

我们可以创建一个表:

CREATE TABLE `t_uq_dipose` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `ref_type` varchar(32) NOT NULL DEFAULT '' COMMENT '关联对象类型',
  `ref_id` varchar(64) NOT NULL DEFAULT '' COMMENT '关联对象id',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_1` (`ref_type`,`ref_id`) COMMENT '保证业务唯一性'
);

对于任何一个业务,有一个业务类型(ref_type),业务有一个全局唯一的订单号,业务来的时候,先查询t_uq_dipose表中是否存在相关记录,若不存在,继续放行。

过程如下:

1.接收到支付宝支付成功请求
2.查询t_uq_dipose(条件ref_id,ref_type),可以判断订单是否已处理

select * from t_uq_dipose where ref_type = '充值订单' and ref_id = trade_no;

3.判断订单是已处理
4.如果订单已处理直接返回,若未处理,继续向下执行
5.打开本地事物
6.给本地系统给用户加钱
7.将订单状态置为成功
8.向t_uq_dipose插入数据,插入成功,提交本地事务,插入失败,回滚本地事务,伪代码:

try{
    insert into t_uq_dipose (ref_type,ref_id) values ('充值订单',trade_no);
    //提交本地事务:
}catch(Exception e){
    //回滚本地事务;
}

说明:
对于同一个业务,ref_type是一样的,当并发时,插入数据只会有一条成功,其他的会违法唯一约束,进入catch逻辑,当前事务会被回滚,最终最有一个操作会成功,从而保证了幂等性操作。
关于这种方式可以写成通用的方式,不过业务量大的情况下,t_uq_dipose插入数据会成为系统的瓶颈,需要考虑分表操作,解决性能问题。
上面的过程中向t_uq_dipose插入记录,最好放在最后执行,原因:插入操作会锁表,放在最后能让锁表的时间降到最低,提升系统的并发性。

关于消息服务中,消费者如何保证消息处理的幂等性?
每条消息都有一个唯一的消息id,类似于上面业务中的trade_no,使用上面的方式即可实现消息消费的幂等性。

总结

1.实现幂等性常见的方式有:悲观锁(for update)、乐观锁、唯一约束
2.几种方式,按照最优排序:乐观锁 > 唯一约束 > 悲观锁

参考链接


接口幂等性这么重要,它是什么?怎么实现?

Using a Thread Pool in Android

Android上使用线程池

In my last post Using HanderThread in Android, I showed how to offload short blocking tasks to a worker thread. While HandlerThread is good for tasks running in a sequential manner, there are cases where background tasks do not have dependencies on each other. To get these tasks done as quickly as possible, you might want to exploit the powerful multi-core processor on the mobile device and run the tasks concurrently on more than one worker thread.

A thread pool is a good fit for this scenario. Thread pool is a single FIFO task queue with a group of worker threads. The producers (E.g. the UI thread) sends tasks to the task queue. Whenever any worker threads in the thread pool become available, they remove the tasks from the front of the queue and start running them.

Comparing with starting a random number of individual worker threads, using a thread pool prevent the overhead of killing and recreating threads every time a worker thread is needed. It also gives you fine control over the number of threads and their lifecycle. E.g. ThreadPoolExecutor allows you to specify how many core threads, how many max threads the pool should create and the keep alive time for the idle threads.

Android supports Java’s Executor framework which offers the following classes for using a thread pool.

  • Executor: an interface which has a execute method. It is designed to decouple task submission from running.
  • Callable: An Interface similar to runnable but allow a result to be returned.
  • Future: Like a promise in JavaScript. It represents the result for an asynchronous task.
  • ExecutorService: an interface which extends Executor interface. It is used to manage threads in the threads pool.
  • ThreadPoolExecutor: a class that implements ExecutorService which gives fine control on the thread pool (Eg, core pool size, max pool size, keep alive time, etc.)
  • ScheduledThreadPoolExecutor: a class that extends ThreadPoolExecutor. It can schedule tasks after a given delay or periodically.
  • Executors: a class that offers factory and utility methods for the aforementioned classes.
  • ExecutorCompletionService: a class that arranges submitted task to be placed on a queue for accessing results.

Basic Thread Pool

The simplest way of creating a thread pool is to use one of the factory methods from Executors class.

static final int DEFAULT_THREAD_POOL_SIZE = 4;

ExecutorService executorService = Executors.newFixedThreadPool(DEFAULT_THREAD_POOL_SIZE);

ExecutorService executorService = Executors.newCachedThreadPool();

ExecutorService executorService = Executors.newSingleThreadExecutor();

newFixedThreadPool creates a thread pool with a a fixed number of thread in the pool specified by the user. The user can call `setCorePoolSized(int)` later to resize the thread pool.

newCachedThreadPool creates a new thread when there is a task in the queue. When there is no tasks in the queue for 60 seconds, the idle threads will be terminated.

newSingleThreadExecutor creates a thread pool with only one thread.

To add a task to the thread pool, call one of the following methods.

executorService.execute(new Runnable(){
  @Override
  public void run(){
    callBlockingFunction();
  }
});

Future future = executorService.submit(new Callable(){
  @Override
  public Object call() throws Exception {
    callBlockingFunction();
    return null;
  }
});

The second method returns a future object. It can be used to retrieve the result from the callable by calling future.get() or cancel the task by calling future.cancel(boolean mayInterruptIfRunning).

Advanced Thread Pool

If you want to have finer control over the thread pool, ThreadPoolExecutor class can be used. In the following example, I first find the available processors of the phone. The thread pool is configured to have core size as the NUMBER_OF_CORES, the maximum core size as the NUMBER_OF_CORES x 2, idle threads’ keep-alive time as 1 second, task queue as a LinkedBlockingQueue object and a custom thread factory.

int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
int KEEP_ALIVE_TIME = 1;
TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;

BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<Runnable>();

ExecutorService executorService = new ThreadPoolExecutor(NUMBER_OF_CORES, 
                                                          NUMBER_OF_CORES*2, 
                                                          KEEP_ALIVE_TIME, 
                                                          KEEP_ALIVE_TIME_UNIT, 
                                                          taskQueue, 
                                                          new BackgroundThreadFactory());
                                                          
private static class BackgroundThreadFactory implements ThreadFactory {
  private static int sTag = 1;

  @Override
  public Thread newThread(Runnable runnable) {
      Thread thread = new Thread(runnable);
      thread.setName("CustomThread" + sTag);
      thread.setPriority(Process.THREAD_PRIORITY_BACKGROUND);

      // A exception handler is created to log the exception from threads
      thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
          @Override
          public void uncaughtException(Thread thread, Throwable ex) {
              Log.e(Util.LOG_TAG, thread.getName() + " encountered an error: " + ex.getMessage());
          }
      });
      return thread;
  }
}

Cancel Tasks

To stop the tasks in the task queue from execution, we just need to clear the task queue. To allow the running threads to be stopped, store all future objects in a list and call cancel on every object which is not done.

// Add a callable to the queue, which will be executed by the next available thread in the pool
public void addCallable(Callable callable){
    Future future = mExecutorService.submit(callable);
    mRunningTaskList.add(future);
}

/* Remove all tasks in the queue and stop all running threads
 * Notify UI thread about the cancellation
 */
public void cancelAllTasks() {
    synchronized (this) {
        mTaskQueue.clear();
        for (Future task : mRunningTaskList) {
            if (!task.isDone()) {
                task.cancel(true);
            }
        }
        mRunningTaskList.clear();
    }
    sendMessageToUiThread(Util.createMessage(Util.MESSAGE_ID, "All tasks in the thread pool are cancelled"));
}

Handle Activity Lifecycle

One thing the thread pool framework does not handle is the Android activity lifecycle. If you want your thread pool to survive the activity lifecycle and reconnect to your activity after it is re-created (E.g. after an orientation change), it needs to be created and maintained outside the activity.

In my example, I made a static singleton class called CustomThreadPoolManager. It has a private constructor. It creates an instance of itself and return that single instance in the static getInstance method. It also holds a weak reference to the Activity. The reference is later used to communicate with the UI thread (see the next section).

public class CustomThreadPoolManager {

    private static CustomThreadPoolManager sInstance = null;
    private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
    private static final int KEEP_ALIVE_TIME = 1;
    private static final TimeUnit KEEP_ALIVE_TIME_UNIT;

    private final ExecutorService mExecutorService;
    private final BlockingQueue<Runnable> mTaskQueue;
    private List<Future> mRunningTaskList;

    private WeakReference<UiThreadCallback> uiThreadCallbackWeakReference;

    // The class is used as a singleton
    static {
        KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
        sInstance = new CustomThreadPoolManager();
    }

    // Made constructor private to avoid the class being initiated from outside
    private CustomThreadPoolManager() {
        // initialize a queue for the thread pool. New tasks will be added to this queue
        mTaskQueue = new LinkedBlockingQueue<Runnable>();
        mRunningTaskList = new ArrayList<>();
        mExecutorService = new ThreadPoolExecutor(NUMBER_OF_CORES, 
                                                NUMBER_OF_CORES*2, 
                                                KEEP_ALIVE_TIME, 
                                                KEEP_ALIVE_TIME_UNIT, 
                                                mTaskQueue, 
                                                new BackgroundThreadFactory());
    }

    public static CustomThreadPoolManager getsInstance() {
        return sInstance;
    }

    ...

    // Keep a weak reference to the UI thread, so we can send messages to the UI thread
    public void setUiThreadCallback(UiThreadCallback uiThreadCallback) {
        this.uiThreadCallbackWeakReference = new WeakReference<UiThreadCallback>(uiThreadCallback);
    }

    ...

}

In the Activity, get the thread pool singleton instance by calling the getInstance static method. Set the activity to the CustomThreadPoolManager. As CustomThreadPoolManager keeps the reference to the Activity as a weak reference, you don’t need to worry about leaking the Activity.

public class MainActivity extends AppCompatActivity implements UiThreadCallback {
    private CustomThreadPoolManager mCustomThreadPoolManager;
    ...
     @Override
    protected void onStart() {
        super.onStart();
        // get the thread pool manager instance
        mCustomThreadPoolManager = CustomThreadPoolManager.getsInstance();
        // CustomThreadPoolManager stores activity as a weak reference. No need to unregister.
        mCustomThreadPoolManager.setUiThreadCallback(this);
    }
    // onClick handler for Send 4 Tasks button
    public void send4tasksToThreadPool(View view) {
        for(int i = 0; i < 4; i++) {
            CustomCallable callable = new CustomCallable();
            callable.setCustomThreadPoolManager(mCustomThreadPoolManager);
            mCustomThreadPoolManager.addCallable(callable);
        }
    }
    ...
}

Communicate with UI Thread

When each task finishes, you may need to send some data back to the UI thread. A safe way of doing this is to send a message to the handler of the UI thread. First, extend Handler class and define what the UI thread should do when a message is received.

UiHandler mUiHandler;
...

@Override
protected void onStart() {
    super.onStart();

    // Initialize the handler for UI thread to handle message from worker threads
    mUiHandler = new UiHandler(Looper.getMainLooper(), mDisplayTextView);
    ...
}
...
// Send message from worker thread to the UI thread
@Override
public void publishToUiThread(Message message) {
    // add the message from worker thread to UI thread's message queue
    if(mUiHandler != null){
        mUiHandler.sendMessage(message);
    }
}
...
// UI handler class, declared as static so it doesn't have implicit
// reference to activity context. This helps to avoid memory leak.
private static class UiHandler extends Handler {
    private WeakReference<TextView> mWeakRefDisplay;

    public UiHandler(Looper looper, TextView display) {
        super(looper);
        this.mWeakRefDisplay = new WeakReference<TextView>(display);
    }

    // This method will run on UI thread
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what){
            // Our communication protocol for passing a string to the UI thread
            case Util.MESSAGE_ID:
                Bundle bundle = msg.getData();
                String messageText = bundle.getString(Util.MESSAGE_BODY, Util.EMPTY_MESSAGE);
                if(null != mWeakRefDisplay) {
                    TextView refDisplay = mWeakRefDisplay.get();
                    if(null != refDisplay) {
                        mWeakRefDisplay.get().append(Util.getReadableTime() + " " + messageText + "\n");
                    }
                }
                break;
            default:
                break;
        }
    }
}

In the CustomThreadPoolManager, use the Activity’s weak reference to send the message to the UI thread.

public void sendMessageToUiThread(Message message){
    if(null != uiThreadCallbackWeakReference) {
        final UiThreadCallback uiThreadCallback = uiThreadCallbackWeakReference.get();
        if(null != uiThreadCallback) {
            uiThreadCallback.publishToUiThread(message);
        }
    }
}

In the CustomCallable, as it has reference to the CustomThreadPoolManager, it can send the message by calling CustomThreadPoolManager’s sendMessageToUiThread method.

@Override
public Object call() throws Exception {
    try {
        // check if thread is interrupted before lengthy operation
        if (Thread.interrupted()) throw new InterruptedException();
    
        // In real world project, you might do some blocking IO operation
        // In this example, I just let the thread sleep for 3 second
        Thread.sleep(3000);
    
        // After work is finished, send a message to CustomThreadPoolManager
        Message message = Util.createMessage(Util.MESSAGE_ID, "Thread " +
                String.valueOf(Thread.currentThread().getId()) + " " +
                String.valueOf(Thread.currentThread().getName()) + " completed");
    
        if(null != mCustomThreadPoolManagerWeakReference) {
                final CustomThreadPoolManager customThreadPoolManager = mCustomThreadPoolManagerWeakReference.get();
                if(null != customThreadPoolManager) {
                    customThreadPoolManager.sendMessageToUiThread(message);
                }
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return null;
}

Source Code

The full source code for the example used in this post is available on Github.

也可本站下载一份 代码拷贝

参考链接


Using a Thread Pool in Android