您的位置:首页 > 汽车 > 新车 > Android小技巧:利用动态代理自动切换线程

Android小技巧:利用动态代理自动切换线程

2024/12/23 20:23:09 来源:https://blog.csdn.net/zssrxt/article/details/139949858  浏览:    关键词:Android小技巧:利用动态代理自动切换线程

日常开发中,多线程编程是个难以避免的话题,开发者可以小心翼翼、谨慎地、严谨地编程来编写出高效的、安全的多线程程序,但是在长时间的维护中,难免因为其中某个人的某个疏忽而导致出现预料之外的并发问题,比如下面这个简单的类:

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;@Overridepublic void showSum(int sum) {tvSum.setText("sum:" + sum);}
}

第二种场景就像下面这段(这里的readFileContent需要在子线程中执行):

public interface WorkerActor {void readFileContent(String path, Consumer<String> consumer);
}class WorkerActorImpl implements WorkerActor {@Overridepublic 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());}@Overridepublic 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;@Overridepublic 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() {@Overridepublic 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 {@Overridepublic 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);@AnyThreadvoid enableLog();
}

相应注解的处理实现另外再写文章阐述。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com