日常开发中,多线程编程是个难以避免的话题,开发者可以小心翼翼、谨慎地、严谨地编程来编写出高效的、安全的多线程程序,但是在长时间的维护中,难免因为其中某个人的某个疏忽而导致出现预料之外的并发问题,比如下面这个简单的类:
class TestActivity extends Activity{
public void refreshView(){
//...
}
}
非常常见的一个类,在Android中只有在主线程可以更新界面,可能一开始写的时候并没有考虑其他线程调用refreshView
方法。然而随着项目推进,终于有一天,某段其他线程的代码出于方便直接调用了这个方法,恰好编写调用的开发者没有来检查这个方法是否线程敏感。
解决这类问题很简单,发现问题的时候补上线程检查或线程切换的代码就好了,但这样没法避免下次犯同样的错误。建立良好的代码规范、谨慎编写并及时review可以减少这类问题的出现,但现实开发中有时候会比较仓促,需要一种比较强制性的、便利的、安全的做法来处理这类隐患。
以前在学习Actor多线程模型的时候,了解到将Actor和线程相关联是一个简单且安全的方案,但如果像Actor模型中那样Actor之间利用消息通信在处理这个问题时就显得有些麻烦,差不多需要重构模块间通信方式了。
那么有没有一种简单粗暴的方法可以像Actor模型那样使得“某个类的方法只在某个线程中执行”并且在“进入这个类的方法时自动切换到对应线程”呢?。本文就介绍一种利用动态代理来完成这个任务。
示例场景
Android开发中有两种非常常见的线程敏感场景:1. 只有主线程可以更新界面;2. 不能在主线程进行IO操作或复杂长时间计算。
第一种场景比如下面这段(这里的showSum
是一个线程敏感方法,必须在主线程调用):
public interface ViewActor {
void showSum(int sum);
}
class ViewActorImpl implements ViewActor {
TextView tvSum;
@Override
public void showSum(int sum) {
tvSum.setText("sum:" + sum);
}
}
第二种场景就像下面这段(这里的readFileContent
需要在子线程中执行):
public interface WorkerActor {
void readFileContent(String path, Consumer<String> consumer);
}
class WorkerActorImpl implements WorkerActor {
@Override
public void readFileContent(String path, Consumer<String> consumer) {
String content = FileIOUtils.readFile2String(path);
consumer.accept(content);
}
}
然而当我们把这些方法标记为public
后,这些方法就可能在任意线程执行。
利用代理来确保在正确的线程调用
上面程序都按照“面向接口编程”来写的,这样我们就很容易创建出一个静态代理提供出去,而不是将实现类提供出去,比如:
class ViewActorProxy implements ViewActor {
private final ViewActor impl;
private final Handler mainHandler;
public ViewActorProxy(ViewActor impl) {
this.impl = impl;
mainHandler = new Handler(Looper.getMainLooper());
}
@Override
public void showSum(int sum) {
mainHandler.post(() -> impl.showSum(sum));
}
}
但如果这个类有10个需要在主线程执行的方法呢?总不能一个个写mainHandler.post
吧,正好Java提供了动态代理,原有程序上,我们可以这样写:
public interface ViewActor {
static ViewActor newInstance() {
Handler mainHandler = new Handler(Looper.getMainLooper());
ViewActorImpl impl = new ViewActorImpl();
return (ViewActor) Proxy.newProxyInstance(ViewActor.class.getClassLoader(), new Class[]{ViewActor.class}, (proxy, method, args) -> {
mainHandler.post(() -> {
try {
method.invoke(impl);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
});
return null;//记得处理equals hashCode等Object方法的调用
});
}
void showSum(int sum);
}
class ViewActorImpl implements ViewActor {
TextView tvSum;
@Override
public void showSum(int sum) {
tvSum.setText("sum:" + sum);
}
}
外部都通过newInstance
方法来获取ViewActor
实例,这样对ViewActorImpl
的调用将被全部切换到主线程中。
注意:实际开发中,更建议使用依赖注入的方式来创建代理,而不是这种直接使用静态方法,这里仅为了方便展示才这么写。
同理,前面的WorkActor
的示例就可以改为:
public interface WorkerActor {
static WorkerActor getInstance() {
ExecutorService executorService = Executors.newCachedThreadPool();
WorkerActorImpl impl = new WorkerActorImpl();
return (WorkerActor) Proxy.newProxyInstance(WorkerActor.class.getClassLoader(), new Class[]{WorkerActor.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
executorService.submit(() -> method.invoke(impl, args));
return null;
}
});
}
void readFileContent(String path, Consumer<String> consumer);
}
class WorkerActorImpl implements WorkerActor {
@Override
public void readFileContent(String path, Consumer<String> consumer) {
String content = FileIOUtils.readFile2String(path);
consumer.accept(content);
}
}
虽说上述示例中将创建代理的代码放在对应接口中,但其实可以把这些代码提取出来封装成工具,例如创建主线程对象的代理可以是:
public class MainThreadProxy {
public static <T> T newProxyFor(Class<T> clazz, T impl) {
Handler mainHandler = new Handler(Looper.getMainLooper());
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, (proxy, method, args) -> {
mainHandler.post(() -> {
try {
method.invoke(impl);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
});
return null;//记得处理equals hashCode等Object方法的调用
});
}
}
这样在其他地方就可以通过一行代码直接得到主线程的代理对象:
ViewActor viewActor = MainThreadProxy.newProxyFor(ViewActor.class, new ViewActorImpl());
viewActor.showSum(1);//这行代码无论在哪个线程调用,最终都在主线程执行
写在最后
Java的动态代理还是有些局限,比如它只能针对接口来创建代理,为了使用代理,有时候我们需要额外定义一个接口。
第二个局限是动态代理对应的接口方法在线程切换时不能带返回值,如果要传递返回值,需要通过CPS/回调的方式,比如:
public interface WorkerActor {
void readFileContent(String path, Consumer<String> consumer);
}
另外,这个方式切换线程默认是以对象为单位的,如果要精确到方法,就需要注解的辅助了,比如:
@ThreadType(ThreadType.IO)
public interface WorkerActor {
void readFileContent(String path, Consumer<String> consumer);
@AnyThread
void enableLog();
}
相应注解的处理实现另外再写文章阐述。
本站资源均来自互联网,仅供研究学习,禁止违法使用和商用,产生法律纠纷本站概不负责!如果侵犯了您的权益请与我们联系!
转载请注明出处: 免费源码网-免费的源码资源网站 » Android小技巧:利用动态代理自动切换线程
发表评论 取消回复