请问谁知道CODE DE Rx是什么日本化妆品品牌大全?

谁知道黑卡好还是微单好?目前看上rx100 m3和m2 有何区别?
全部答案(共4个回答)
也得选有取景器的 比如a6000 否则就那一块屏幕光线强的环境特悲剧
不想向专业水平发展的话,女孩子还是选择RX100好些,小巧、精致、画质色彩鲜艳,还有好多照片效果,不用修图就能完成特效,高ISO噪点控制好,ISO1600完全可...
3多了一个取景器 焦段也更广了 光圈也更大了 提升的不少
佳能700d不如泥坑d5300,佳能就个触摸屏有亮点,其实这个价位的入门单反和同价位的微单画质相差无几,便携性却差许多。黑卡也是挺不错,大底卡片,就是不能更换镜...
相机打开WIFI 手机安装软件 官网下载的 然后开启WIFI 接受相机信号
可以格式化删除,也可以按文件夹删除
答: 应该会的
答: 数码相机产品分类编辑数码相机单反相机单反数码相机就尼康(Nikon)D7000单反相机是指单镜头反光数码相机,即digital数码、single单独、lens镜...
答:   光学防抖
  光学防抖是利用安装在镜头里的一组可以上下左右活动的镜片(PSD镜片)来完成的。当手发生抖动时,检测电路检测出抖动的方向,经控制电路控制PSD镜...
答: 在洛杉矶购物都要加8.25的消费税,所以买电器类的产品不一定划算.
一般人到美国都是买鱼油,维他命类保健品,化妆品之类的.看你需要了.
大家还关注
Copyright &
Corporation, All Rights Reserved
确定举报此问题
举报原因(必选):
广告或垃圾信息
激进时政或意识形态话题
不雅词句或人身攻击
侵犯他人隐私
其它违法和不良信息
报告,这不是个问题
报告原因(必选):
这不是个问题
这个问题分类似乎错了
这个不是我熟悉的地区
相关问答:123456789101112131415微信扫一扫 咨讯我知晓
DSCN5640.JPG (516.85 KB, 下载次数: 8)
11:59 上传
自从走了英淘直邮后,很久没有用转用发过护肤品了。这次受人之托要买timeless的产品,只能发了转运。用了华美快递。从发货到收获的速度还是很快的。10/19提交转运信息,10/29收到产品入手。因为外箱完全用封箱带粘住,所以从外包装来看产品没有问题。
DSCN5642.JPG (550.2 KB, 下载次数: 8)
11:59 上传
打开一看,坏了,箱子湿了,说明有产品泄漏了。
DSCN5644.JPG (566.56 KB, 下载次数: 9)
11:59 上传
果然,泄漏的是这瓶东西,纸包装已经全湿了。
DSCN5646.JPG (548.63 KB, 下载次数: 8)
12:00 上传
打开一看,是玻璃瓶破损,整瓶东西完全泄漏出来了。
DSCN5647.JPG (679.65 KB, 下载次数: 13)
12:00 上传
产品的全家福,上面一排购于skincarerx,下面一排购于timeless skin care。
DSCN5648.JPG (581.25 KB, 下载次数: 8)
12:00 上传
拆了外包装后的产品。分别是:
上排左起:
1.skincarerx当时购物满75刀赠送的Vivite travel kit。里面包含洁面乳8ml,抗氧化面部精华5ml,眼霜7g,晚霜7g。里面还有一把小小的挖勺。挺有意思的一个套装。
2. dermalogica spf30防晒霜小样,到2015/3月过期。
3. natura bisse的清洁面膜小样2ml,这个生产批号没有看懂。
这两个小样也是skincarerx赠送的,它家的小样不能选择,是随机发送的。
1. Erbaviva的柠檬味止汗剂旅行装30ml。
2. Juice Beauty的抗氧化精华60ml。2015/11月到期。
3. Timeless的hyaluronic acid serum 100% pure 60ml。买了两瓶,破损了1瓶。
4. Timeless的argan oil 100% pure 30ml。也是买了两瓶,无破损。
5. Juice Beauty的保湿喷雾 200ml。2016/06到期。
6. Alaska Glacial Mud净化矿物泥面膜 (香草薰衣草味)100ml。
没写到期日的都是因为代码没看懂。
除了timeless的产品,剩余买入的正品都是在skincarerx里的skinbotanica买的有机产品。
国内在skincarerx上买东西的人不多,这个网站最大的优点就是几乎全年每一天都有折扣,不用像其他网站一样等折扣。也支持国卡和转运,缺点是有些品牌的产品不是很全。但同样的产品在各种折扣力度下,应该是全美最低价了。有兴趣的MM可以去看看。
这单就晒到这里啦,所谓有去有得,速度确实很快,盒箱也给力。这单正好控制在3磅。除了产品破损以外。目前转运是说不能赔付了,不过我也写了邮件给timeless公司,不知他们会如何处理。
(512.28 KB, 下载次数: 8)
11:59 上传
(553.37 KB, 下载次数: 8)
11:59 上传
(552.57 KB, 下载次数: 7)
11:59 上传
(475.72 KB, 下载次数: 8)
12:00 上传
哇哇哇,对timeless的产品很感兴趣的
早两年就知道他家pure口碑了,你在美亚买的吗?
木木凡 发表于
哇哇哇,对timeless的产品很感兴趣的
早两年就知道他家pure口碑了,你在美亚买的吗? ...
不是,我在它家官网买的。。官网上买2瓶以上就有折扣,免美国国内运费。很白菜价呀。
就是这次碎了一瓶,要重新再买了。
cali197904 发表于
不是,我在它家官网买的。www.timelessha.com。官网上买2瓶以上就有折扣,免美国国内运费。很白菜价呀。
恩恩,我以前只知道美亚有嘞
这个划算的
你timeless邮寄的时候有没有要求加固什么啊?
木木凡 发表于
恩恩,我以前只知道美亚有嘞
这个划算的
你timeless邮寄的时候有没有要求加固什么啊? ...
没有呀,转运直邮了那么多次,从来没有发生玻璃瓶破碎的。送到转运我想肯定是没有碎的。这次正好放在箱子的最上方,我猜想上面压过重物不巧压碎了。好在价值不高,也算给自己提个醒了。
楼主,这家有啥好货啊?
能不能推荐一下呢~
第一次接触这个牌子~
louise89929 发表于
楼主,这家有啥好货啊?
能不能推荐一下呢~
第一次接触这个牌子~
你是指的哪个?timeless家?
cali197904 发表于
不是,我在它家官网买的。www.timelessha.com。官网上买2瓶以上就有折扣,免美国国内运费。很白菜价呀。
为什么我付款的时候没有显示她首页的折扣呢?可以告诉我怎么操作吗?
第一次接触timeless这个牌子, 楼主入介紹一下这个牌子有啥好货
cali197904 发表于
你是指的哪个?timeless家?
我上网站看,好像商品不多,楼主晒的某几款都没有见到呢~
手机客户端
扫码下载APPCode杂货铺Code杂货铺生命不息,折腾不止关注专栏更多最新文章{&debug&:false,&apiRoot&:&&,&paySDK&:&https:\u002F\u002Fpay.zhihu.com\u002Fapi\u002Fjs&,&wechatConfigAPI&:&\u002Fapi\u002Fwechat\u002Fjssdkconfig&,&name&:&production&,&instance&:&column&,&tokens&:{&X-XSRF-TOKEN&:null,&X-UDID&:null,&Authorization&:&oauth c3cef7c66aa9e6a1e3160e20&}}{&database&:{&Post&:{&&:{&title&:&译-使用RxJava从多个数据源加载数据&,&author&:&lsxiao&,&content&:&\u003Cp\u003E译自\u003Ca href=\&http:\u002F\u002Fblog.danlew.net\u002F\u002F22\u002Floading-data-from-multiple-sources-with-rxjava\u002F\& data-editable=\&true\& data-title=\&Loading data from multiple sources with RxJava\&\u003ELoading data from multiple sources with RxJava\u003C\u002Fa\u003E,翻译得比较烂,能看懂原理就好。\u003C\u002Fp\u003E\u003Cp\u003E假设现在有些数据需要通过网络才能查询获取。虽然在每次我需要数据的时候,我可以通过netwrok简单的获取到,但是如果把这些数据缓存到disk和memory里会使我们的获取数据变得更加高效。\u003C\u002Fp\u003E\u003Cp\u003E更具体的说,我想建立这样的数据加载模型:\u003Cbr\u003E1.偶尔从网络进行一次查询来刷新数据\u003Cbr\u003E2.尽可能快的用其它方式获取数据(获取缓存的网络数据)\u003C\u002Fp\u003E\u003Cp\u003E我想使用RxJava实现这个模型。\u003C\u002Fp\u003E\u003Ch1\u003E基本形式\u003C\u002Fh1\u003E\u003Cp\u003E假设我们有来自每个数据源的Observable对象(network,disk,memory),我们可以使用concat()和first()两个操作符来构建一个简单的解决方案。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cp\u003Econcat()操作符可以接受多个Observable作为参数,把它们合并成为一个Observable,并且连接合并Observable的事件队列。\u003C\u002Fp\u003E\u003Cp\u003Efirst()只会获取到Observable的事件队列发出第一个事件。因此,通过使用conact().first(),我们可以获取到多个数据源发出的第一个事件。\u003C\u002Fp\u003E\u003Cp\u003E让我们在实际例子中来看看:\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003E\u002F\u002F Our sources (作为练习留给读者)\nObservable&Data& memory = ...;
\nObservable&Data& disk = ...;
\nObservable&Data& network = ...;\n\n\u002F\u002F Retrieve the first source with data\nObservable&Data& source = Observable
.concat(memory, disk, network)\n
.first();\u003C\u002Fcode\u003E\u003Cp\u003E这个模型的关键是,只在需要的时候,concat()才会订阅每个子Observable。在first()执行后,事件队列会及早的停止事件的发出,如果数据已经缓存过了,就没有必要从更耗时的数据源获取数据。\u003C\u002Fp\u003E\u003Cp\u003E换句话说,如果Memory Observable返回了结果,之后我们就不需要从Disk或者Network获取数据。相反,如果Disk数据源和Memory数据源都没有数据,那么会从Network获取数据。\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E\u003Cb\u003E注意 \u003C\u002Fb\u003Econtact()方法传入参数的顺序,这个方法返回的Observable对象的事件队列是根据传入的Observable参数的事件队列顺序连接的。\u003C\u002Fp\u003E\u003Ch1\u003E保存数据\u003C\u002Fh1\u003E\u003Cp\u003E很明显地,下一步我们需要在获取到数据的时候把它们保存下来。如果你不把通过网络请求获取到的数据保存到disk或者memory中。那么这里不会有任何存储的数据存在!而以上所有的代码都将会不断的重复网络请求。\u003C\u002Fp\u003E\u003Cp\u003E我的解决方法是在每一个数据源输出数据的时候保存或者缓存它们。\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003EObservable&Data& networkWithSave = network.doOnNext(data -& {
saveToDisk(data);\n
cacheInMemory(data);\n});\n\nObservable&Data& diskWithCache = disk.doOnNext(data -& {
cacheInMemory(data);\n});\u003C\u002Fcode\u003E\u003Cp\u003E现在,你可以使用networkWithSave和diskWithCache,在你加载数据的时候,它们会自动的存储到disk或者memory。\u003C\u002Fp\u003E\u003Cp\u003E(这种方式的另一个好处就是networkWithSave\u002FdiskWithCache,除了使用在多个数据源的模式里面,还可以在其它地方使用)\u003C\u002Fp\u003E\u003Ch2\u003E过期数据\u003C\u002Fh2\u003E\u003Cp\u003E不幸的是,现在我们保存数据的代码运行得有点太好了!因为无论数据是否已经变更,它总是返回同样的数据(缓存过一次后就不会再请求服务端的数据了)。但是,我们偶尔想从服务器获得数据。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cp\u003Efirst()可以执行数据过滤,所以可以在first()中进行判断:\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003EObservable&Data& source = Observable
.concat(memory, diskWithCache, networkWithSave)\n
.first(data -& data.isUpToDate());\n\u003C\u002Fcode\u003E\u003Cp\u003E现在,Observable只会发出符合我们筛选条件的第一个数据源的事件。因此,如果我们的Observable有过时的数据,first将切换并继续判断下一个Observable的data,直到找到新的数据。\u003C\u002Fp\u003E\u003Ch2\u003Efirst() vs. takeFirst()\u003C\u002Fh2\u003E你也可以使用takeFirst()来代替first()\u003Cp\u003E它们的不同在于,如果没有任何一个Observable发出有效的数据,first()将会抛出一个NoSuchElementException,而takeFirst则不会。\u003C\u002Fp\u003E\u003Cp\u003E使用first()还是takeFirst,取决于你是否需要处理数据不存在的异常。\u003C\u002Fp\u003E\u003Ch1\u003ECode Samples\u003C\u002Fh1\u003E\u003Cp\u003E这是上面的代码实现:\u003Ca href=\&https:\u002F\u002Fgithub.com\u002Fdlew\u002Frxjava-multiple-sources-sample\& data-editable=\&true\& data-title=\&sample\&\u003Esample\u003C\u002Fa\u003E\u003C\u002Fp\u003E\u003Cp\u003E如果你需要一个实际开发中的示例,你可以看看\u003Ca href=\&https:\u002F\u002Fgithub.com\u002Fdlew\u002Fandroid-gfycat\& data-editable=\&true\& data-title=\&the Gfycat app\& class=\&\&\u003Ethe Gfycat app\u003C\u002Fa\u003E。\u003C\u002Fp\u003E&,&updated&:new Date(&T11:33:23.000Z&),&canComment&:false,&commentPermission&:&anyone&,&commentCount&:4,&likeCount&:6,&state&:&published&,&isLiked&:false,&slug&:&&,&isTitleImageFullScreen&:true,&rating&:&none&,&sourceUrl&:&&,&publishedTime&:&T19:33:23+08:00&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&url&:&\u002Fp\u002F&,&titleImage&:&https:\u002F\u002Fpic1.zhimg.com\u002Fv2-3b9b630d9f6f24f1ddeca8_r.jpg&,&summary&:&&,&href&:&\u002Fapi\u002Fposts\u002F&,&meta&:{&previous&:null,&next&:null},&snapshotUrl&:&&,&commentsCount&:4,&likesCount&:6},&&:{&title&:&Apollo-基于编译时注解处理的RxBus实现&,&author&:&lsxiao&,&content&:&\u003Cp\u003E在这篇教程中,我会向您展现如何实现一个编译时注解处理的RxBus(事件总线)。\u003C\u002Fp\u003E\u003Ch2\u003E\u003Cb\u003E开始之前\u003C\u002Fb\u003E\u003C\u002Fh2\u003E\u003Cp\u003E由于本篇文章会比较长,先容许我申明一些问题。\u003C\u002Fp\u003E\u003Cp\u003E本篇文章是建立在您对Java Annotation以及Annotation Process 熟悉的基础之上,且本篇文章不讨论那些在运行时(Runtime)通过反射获取注解实现的RxBus,只讨论在编译时(Compile)完成的注解处理。\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E如果您实在没有耐心看完本篇文章,您可以点击 \u003Ca href=\&https:\u002F\u002Flink.zhihu.com\u002F?target=https%3A\u002F\u002Fgithub.com\u002Flsxiao\u002FApollo\& class=\&\& data-editable=\&true\& data-title=\&Apollo\&\u003EApollo\u003C\u002Fa\u003E 访问Github上的开源库直接阅读我的源码,截止于日02:00,Github上Apollo的版本为0.1.2。\u003C\u002Fp\u003E\u003Cp\u003E注意!目前 \u003Ca href=\&https:\u002F\u002Flink.zhihu.com\u002F?target=https%3A\u002F\u002Fgithub.com\u002Flsxiao\u002FApollo\& class=\&\& data-editable=\&true\& data-title=\&Apollo\&\u003EApollo\u003C\u002Fa\u003E 仍处于非稳定版本,0.2版本之前不建议用于生产环境。\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E本篇文章以下提及到的RxBus的实现统称Apollo。\u003C\u002Fp\u003E\u003Ch2\u003E\u003Cb\u003ECompile-time的好处\u003C\u002Fb\u003E\u003C\u002Fh2\u003E\u003Cp\u003E目前Android中的一些流行库,如Dagger2,ButterKnife等都使用了Compile-time的注解处理,在编译时生成注入代码,来实现注入功能。其实通过Java的反射机制,也可以实现同样的功能,而且实现更加简单方便,不过反射机制的性能是一个问题,大量使用反射,往往是APP严重性能问题的根本原因。\u003C\u002Fp\u003E\u003Cp\u003E而编译时注解处理只会在编译的时候占用开发资源,生成额外的代码来实现功能,这些通过注解处理生成的Java源代码会同其他手写的源文件一同被编译进APK。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ch2\u003EApollo\u003Cb\u003E的实现\u003C\u002Fb\u003E\u003C\u002Fh2\u003E\u003Cp\u003E目前常用的EventBus,Otto都是基于观察者(发布\u002F订阅)模式。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cimg src=\&87d7f0f7e01b188aa312c91b2be45fe8.png\& data-rawwidth=\&1280\& data-rawheight=\&479\&\u003E所以,对于RxJava来说,它也可以实现事件总线,因为它本生就是基于观察者模式的。\u003C\u002Fp\u003E\u003Cp\u003E我们知道RxJava中Subject同时充当了Observer和Observable的角色,我们即可以通过Subject来发布事件,同时也可以通过Subject来接收事件。\u003C\u002Fp\u003E\u003Cp\u003E在RxJava提供的几个Subject中,PublishSubject最符合目前所需。\u003C\u002Fp\u003E\u003Cblockquote\u003EPublishSubject只会把在订阅发生的时间点之后来自原始Observable的数据发射给观察者\u003Cbr\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E在编写代码前,我希望Apollo能够做到以下几点:\u003C\u002Fp\u003E\u003Col\u003E\u003Cli\u003E事件都有Tag作为唯一标识,\u003Cbr\u003E\u003C\u002Fli\u003E\u003Cli\u003E订阅者只接收对应Tag的事件\u003Cbr\u003E\u003C\u002Fli\u003E\u003Cli\u003E事件可以是任何对象\u003C\u002Fli\u003E\u003Cli\u003E支持sticky event功能(因为在事件发送的时候,可能订阅此事件的Activity还没有启动)\u003C\u002Fli\u003E\u003Cli\u003EApollo是单例的\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003Cp\u003E在满足以上条件后,先开始编写不带注解功能的Apollo:\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&text\&\u003Epackage com.lsxiao.\n\n\nimport com.lsxiao.apllo.annotations.R\nimport com.lsxiao.apllo.entity.SubscriberE\nimport com.lsxiao.apllo.entity.SubscriptionB\n\nimport java.util.ArrayL\nimport java.util.A\nimport java.util.HashM\nimport java.util.L\nimport java.util.M\nimport java.util.concurrent.ConcurrentHashM\n\nimport rx.O\nimport rx.S\nimport rx.functions.Func1;\nimport rx.schedulers.S\nimport rx.subjects.PublishS\nimport rx.subjects.SerializedS\n\n\u002F**\n * author lsxiao\n * date
17:27\n *\u002F\npublic class Apollo {\n
private SerializedSubject&SubscriberEvent, SubscriberEvent& mPublishS\n
private final Map&String, SubscriberEvent& mStickyEventM\u002F\u002F用于保存stick事件\n
private static Apollo sI\n\n
private Apollo() {\n
\u002F\u002FSerializedSubject是线程安全的\n
\u002F\u002FPublishSubject 会发送订阅者从订阅之后的事件序列,这意味着没订阅前的事件序列不会被发送到当前订阅者\n
mPublishSubject = new SerializedSubject&&(PublishSubject.&SubscriberEvent&create());\n
mStickyEventMap = new ConcurrentHashMap&&();\n
mBindTargetMap = new HashMap&&();\n
\u002F**\n
* 返回一个Apollo的单例对象\n
* @return Apollo\n
public synchronized static Apollo get() {\n
if (null == sInstance) {\n
sInstance = new Apollo();\n
return sI\n
\u002F**\n
* 判断是否有订阅者\n
public boolean hasObservers() {\n
return mPublishSubject.hasObservers();\n
\u002F**\n
* 根据tag和eventType获取指定类型的Sticky事件\n
public &T& T getStickyEvent(String tag, Class&T& eventType) {\n
synchronized (mStickyEventMap) {\n
Object o = mStickyEventMap.get(tag).getData();\n
if (o.getClass().getCanonicalName().equals(eventType.getCanonicalName())) {\n
return eventType.cast(o);\n
\u002F**\n
* 根据tag获取Sticky事件\n
public Object getStickyEvent(String tag) {\n
synchronized (mStickyEventMap) {\n
return mStickyEventMap.get(tag) == null ? null : mStickyEventMap.get(tag).getData();\n
\u002F**\n
* 移除指定eventType的Sticky事件\n
public void removeStickyEvent(String tag) {\n
synchronized (mStickyEventMap) {\n
mStickyEventMap.remove(tag);\n
\u002F**\n
* 移除指定eventType的Sticky事件\n
public void removeStickyEvent(String[] tags) {\n
for (String tag : tags) {\n
removeStickyEvent(tag);\n
\u002F**\n
* 移除所有的Sticky事件\n
public void removeAllStickyEvents() {\n
synchronized (mStickyEventMap) {\n
mStickyEventMap.clear();\n
\u002F**\n
* 发送event\n
* @param event SubscriberEvent\n
private void send(SubscriberEvent event) {\n
mPublishSubject.onNext(event);\n
\u002F**\n
* 发送event\n
* @param tag
* @param actual 内容\n
public void send(String tag, Object actual) {\n
SubscriberEvent event = new SubscriberEvent(tag, actual);\n
send(event);\n
\u002F**\n
* 发送只有tag的event\n
* @param tag
public void send(String tag) {\n
send(tag, new Object());\n
\u002F**\n
* 发送一个新Sticky事件\n
public void sendSticky(String tag, Object actual) {\n
synchronized (mStickyEventMap) {\n
SubscriberEvent event = new SubscriberEvent(tag, actual, true);\n
mStickyEventMap.put(tag, event);\n
send(event);\n
public void sendSticky(String tag) {\n
synchronized (mStickyEventMap) {\n
SubscriberEvent event = new SubscriberEvent(tag, new Object(), true);\n
mStickyEventMap.put(tag, event);\n
mPublishSubject.onNext(event);\n
public Observable&Object& toObservable(final String tag) {\n
return toObservable(new String[]{tag}, Object.class);\n
public Observable&Object& toObservable(final String[] tags) {\n
return toObservable(tags, Object.class);\n
\u002F**\n
* 返回普通事件类型的被观察者\n
* @param eventType 只接受eventType类型的响应,ofType = filter + cast\n
* @return Observable\n
public &T& Observable&T& toObservable(final String tag, final Class&T& eventType) {\n
return toObservable(new String[]{tag}, eventType);\n
public &T& Observable&T& toObservable(final String[] tags, final Class&T& eventType) {\n
if (null == eventType) {\n
throw new NullPointerException(\&the eventType must be not null\&);\n
if (null == tags) {\n
throw new NullPointerException(\&the tags must be not null\&);\n
if (0 == tags.length) {\n
throw new IllegalArgumentException(\&the tags must be not empty\&);\n
return mPublishSubject\n
.filter(new Func1&SubscriberEvent, Boolean&() {\n
@Override\n
public Boolean call(SubscriberEvent subscriberEvent) {\n
return Arrays.asList(tags).contains(subscriberEvent.getTag()) &&\n
\u002F\u002F如果subscriberEvent.getData() = null,不用再去检查是不是特定类型或者其子类的实例\n
(subscriberEvent.getData() == null || eventType.isInstance(subscriberEvent.getData()));\n
.flatMap(new Func1&SubscriberEvent, Observable&T&&() {\n
@Override\n
public Observable&T& call(SubscriberEvent subscriberEvent) {\n
return Observable.just(eventType.cast(subscriberEvent.getData()));\n
public Observable toObservableSticky(final String tag) {\n
return toObservable(new String[]{tag});\n
public Observable&Object& toObservableSticky(final String[] tags) {\n
return toObservableSticky(tags, Object.class);\n
\u002F**\n
* 根据传递的 eventType 类型返回特定类型(eventType)的 被观察者\n
public &T& Observable&T& toObservableSticky(String tag, final Class&T& eventType) {\n
return toObservableSticky(new String[]{tag}, eventType);\n
public &T& Observable&T& toObservableSticky(final String[] tags, final Class&T& eventType) {\n
if (null == eventType) {\n
throw new NullPointerException(\&the eventType must be not null\&);\n
if (null == tags) {\n
throw new NullPointerException(\&the tags must be not null\&);\n
if (0 == tags.length) {\n
throw new IllegalArgumentException(\&the tags must be not empty\&);\n
synchronized (mStickyEventMap) {\n
\u002F\u002F普通事件的被观察者\n
Observable&T& observable = toObservable(tags, eventType);\n\n
final List&SubscriberEvent& stickyEventList = new ArrayList&&();\n
for (String tag : tags) {\n
\u002F\u002Fsticky事件\n
final SubscriberEvent event = mStickyEventMap.get(tag);\n
if (event != null) {\n
stickyEventList.add(mStickyEventMap.get(tag));\n
if (!stickyEventList.isEmpty()) {\n
\u002F\u002F合并事件序列\n
return Observable.from(stickyEventList)\n
.flatMap(new Func1&SubscriberEvent, Observable&T&&() {\n
@Override\n
public Observable&T& call(SubscriberEvent subscriberEvent) {\n
return Observable.just(eventType.cast(subscriberEvent.getData()));\n
}).mergeWith(observable);\n\n
} else {\n
}\n}\n\u003C\u002Fcode\u003E\u003Cul\u003E\u003Cli\u003E上述代码中Subject同时充当了Observable以及Observer的功能,同时我们使用强制类型转换将PublishSubject转换成线程安全的SerializedSubject。\u003Cbr\u003E\u003C\u002Fli\u003E\u003Cli\u003E由于我们要实现通过tag接收event,以及sticky event功能,所以将tag和event封装成SubscriberEvent对象\u003C\u002Fli\u003E\u003Cli\u003E我们在sendSticky()方法中将SubscriberEvent缓存起来,以便在有订阅者订阅sticky事件的时候,能够发送出去。\u003C\u002Fli\u003E\u003Cli\u003E注意,toObservableSticky方法中对SubscriberEvent到源事件之间的转换,以及为了在接收到Sticky事件后还能继续接收普通事件,将普通的Observable和stickyObservable进行merge的操作。\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Ch2\u003E\u003Cb\u003E思路分析\u003C\u002Fb\u003E\u003C\u002Fh2\u003E\u003Cp\u003E首先,创建一个同时可以充当Observable和Observer的Subject作为事件发布和订阅的桥梁。\u003C\u002Fp\u003E\u003Cp\u003E在Acticity,Fragment等需要接收事件的类中实现订阅事件方法,例如:\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&text\&\u003EApollo.get().toObservable(User.class)\n
.subscribe(user -& {\n
\u002F\u002Fdo something.\n
}, throwable -& {\n
throwable.printStackTrace();\n
}));\n\u003C\u002Fcode\u003E\u003Cp\u003E在我们需要发布事件的地方进行发布\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&text\&\u003EApollo.instance().send(\&SHOW_USER_INFO\&,user);\n\u003C\u002Fcode\u003E\u003Cbr\u003E\u003Cp\u003E这样,在发布事件之前所有订阅了\&SHOW_USER_INFO\&普通事件的订阅者,都能够接收到\&SHOW_USER_INFO\&事件。\u003C\u002Fp\u003E\u003Cp\u003E如果此事件发布之前订阅者还未订阅,我们可以发布一个Sticky事件:\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&text\&\u003EApollo.instance().sendSticky(\&SHOW_USER_INFO\&,user);\n\u003C\u002Fcode\u003E\u003Cp\u003E同时在订阅处通过toObservableSticky来订阅:\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&text\&\u003EApollo.get().toObservableSticky(User.class)\n
.subscribe(user -& {\n
\u002F\u002Fdo something.\n
}, throwable -& {\n
throwable.printStackTrace();\n
}));\n\u003C\u002Fcode\u003E\u003Cp\u003E这样在订阅者订阅后,第一时间就会接收到此Sticky事件。注意,Sticky事件需要自行手动清除。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cb\u003E且注意在Activity\u002FFragment生命周期结束时候,一定要需要取消订阅,以防止RxJava引起的内存泄露。\u003C\u002Fb\u003E\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&text\&\u003E@Override\nprotected void onDestroy() {\n
super.onDestroy();\n
if(!subscription.isUnsubscribed()) {\n
subscription.unsubscribe();\n
}\n}\n\u003C\u002Fcode\u003E\u003Cp\u003E上述代码已经实现了一个简单的RxBus。\u003C\u002Fp\u003E\u003Ch2\u003E\u003Cb\u003E基于编译时注解处理的\u003C\u002Fb\u003EApollo实现\u003C\u002Fh2\u003E\u003Cp\u003E还记得上面的Apollo的功能要求吗?\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Col\u003E\u003Cli\u003E事件都有Tag作为唯一标识,\u003Cbr\u003E\u003C\u002Fli\u003E\u003Cli\u003E订阅者只接收对应Tag的事件\u003Cbr\u003E\u003C\u002Fli\u003E\u003Cli\u003E事件可以是任何对象\u003C\u002Fli\u003E\u003Cli\u003E支持sticky event功能(因为在事件发送的时候,可能订阅此事件的Activity还没有启动)\u003C\u002Fli\u003E\u003Cli\u003EApollo是单例的\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E现在,我要在之前的基础在给Apollo增加一些新的功能\u003C\u002Fp\u003E\u003Cbr\u003E\u003Col\u003E\u003Cli\u003E我现在想通过Annotation来简化订阅代码,实现订阅指定tag事件:\u003C\u002Fli\u003E\u003Cli\u003E\u003Ccode lang=\&text\&\u003E@Receive(tag =\&SHOW_USER_INFO\&)\npublic void receiveUser(User user) {\n
Log.d(\&apollo\&, \&receive user event\& + user.toString());\n}\n\u003C\u002Fcode\u003E\u003C\u002Fli\u003E\u003Cli\u003E通过Annotation控制接收事件的类型(sticky,normal)\u003Cbr\u003E\u003Ccode lang=\&text\&\u003E@Receive(tag =\&SHOW_USER_INFO\&,type=Receive.Type.STICKY)\npublic void receiveUser(User user) {\n
Log.d(\&apollo\&, \&receive user event\& + user.toString());\n}\n\u003C\u002Fcode\u003E\u003C\u002Fli\u003E\u003Cli\u003E通过Annotation控制调度器\u003Cbr\u003E\u003Ccode lang=\&text\&\u003E@Receive(tag =\&SHOW_USER_INFO\&,\nsubscribeOn=Receive.Thread.IO,\nobserveOn=Receive.Thread.Main)\npublic void receiveUser(User user) {\n
Log.d(\&apollo\&, \&receive user event\& + user.toString());\n}\n\u003C\u002Fcode\u003E\u003C\u002Fli\u003E\u003Cli\u003E通过以下的形式来进行绑定和解绑\u003Cbr\u003E\u003Ccode lang=\&text\&\u003Epublic abstract class BaseActivity extends AppCompatActivity {\n
private SubscriptionBinder mB\n\n
@Override\n
protected void onCreate(Bundle savedInstanceState) {\n
super.onCreate(savedInstanceState);\n
setContentView(getLayoutId());\n
afterCreate(savedInstanceState);\n
\u002F\u002F绑定\n
mBinder = Apollo.get().bind(this);\n
@Override\n
protected void onDestroy() {\n
super.onDestroy();\n
\u002F\u002F解绑\n
mBinder.unbind();\n
protected abstract int getLayoutId();\n\n
protected abstract void afterCreate(Bundle savedInstanceState);\n}\n\u003C\u002Fcode\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003Cbr\u003E由于开篇已经说过本篇文章只介绍如何实现Complie-time的Apollo,所以这里不考虑Run-time的反射机制实现。\u003Cp\u003E在开始编写complie-time Apollo之前,我们先回顾一下Dagger2和ButterKnife,当使用的时候至少都需要引入两个module,其中有一个module一般叫做compiler或者processor(以下统称processor),例如:\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&text\&\u003Ecompile 'com.jakewharton:butterknife:8.2.1'\napt 'com.jakewharton:butterknife-compiler:8.2.1'\n\u003C\u002Fcode\u003E\u003Cp\u003Eprocessor的作用是负责完成注解处理,生成代码。\u003C\u002Fp\u003E\u003Cp\u003E同时由于编译时用于生成代码的processor我们是不需要打包进apk的,所以使用apt而不是complie命令。\u003C\u002Fp\u003E\u003Cp\u003E在这里,我把整个Apollo分成两个部分:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E一个是核心代码部分,实现主要的功能,提供绑定接口的apollo module。\u003Cbr\u003E\u003C\u002Fli\u003E\u003Cli\u003E一个是解析所有我们需要Annotation,生成指定绑定代码的processor module。\u003Cbr\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cbr\u003E\u003Cp\u003E在编写processor之前,其实apollo的逻辑之前基本上已经完成的差不多了,这里主要需要实现的还剩:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003Eapollo提供绑定接口,让processor生成的绑定代码实现接口,初始化的时候让apollo持有生成 的绑定接口实现类实例。这样就可以在BaseActivity中通过apollo.get.bind()方式进行绑定。\u003Cbr\u003E\u003C\u002Fli\u003E\u003Cli\u003Eapollo提供RxJava的基本调度器,而AndroidSchedulers.mainThread()在初始化的时候作为参数传入。(由于apollo module是一个java library,不能依赖作为Android library的RxAndroid,所以main调度器需要初始化的时候传入,之后我会提及到为什么apollo和processor都只能是java library)\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E这里先给出SubscriberBinder接口:\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&text\&\u003E
public interface SubscriberBinder {\n
SubscriptionBinder bind(Object object);\n
}\n\u003C\u002Fcode\u003E\u003Cp\u003E接口很简单,只需要实现一个bind方法,并且返回SubscriptionBinder对象(注意和SubscriberBinder的区别)。\u003C\u002Fp\u003E\u003Cp\u003ESubscriberBinder的实现(用于取消订阅)\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&text\&\u003Epublic class SubscriptionBinder {\n
private CompositeSubscription mS\n\n
public SubscriptionBinder() {\n
mSubscription = new CompositeSubscription();\n
public void add(Subscription subscription) {\n
if (mSubscription == null) {\n
throw new IllegalAccessError(\&this binder has been unbinded\&);\n
if (subscription == null) {\n
throw new NullPointerException(\&subscription must be not null\&);\n
mSubscription.add(subscription);\n
public void unbind() {\n
if (mSubscription != null && !mSubscription.isUnsubscribed()) {\n
mSubscription.unsubscribe();\n
mSubscription =\n
}\n}\n\u003C\u002Fcode\u003E\u003Cbr\u003E\u003Cp\u003E给Apollo添加绑定方法\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&text\&\u003Epublic SubscriptionBinder bind(Object o) {\nif (null == o) {\nthrow new NullPointerException(\&object to bind must not be null\&);\n
}\nreturn mSubscriberBinder.bind(o);\n}\n\u003C\u002Fcode\u003E绑定成功后,返回一个SubscriptionBinder对象,在生命周期结束的时候我们可以调用SubscriptionBinder.unbind();方法来解绑当前Activity或者Fragment中所有的订阅者。\u003Cbr\u003E\u003Cp\u003E\u003Cb\u003Eprocessor实现\u003C\u002Fb\u003E\u003C\u002Fp\u003E\u003Cp\u003E在写processor之前,首先要知道AbstractProcessor这个抽象处理器,每一个注解处理器都必须继承此处理器(下面会有很多引用解释,不过不会赘述):\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&text\&\u003Epackage com.\n\npublic class ApolloAnnotationProcessor extends AbstractProcessor {\n\n
@Override\n
public synchronized void init(ProcessingEnvironment env){ }\n\n
@Override\n
public boolean process(Set&? extends TypeElement& annoations, RoundEnvironment env) { }\n\n
@Override\n
public Set&String& getSupportedAnnotationTypes() { }\n\n
@Override\n
public SourceVersion getSupportedSourceVersion() { }\n\n} \n\u003C\u002Fcode\u003E\u003Cul\u003E\u003Cli\u003Einit(ProcessingEnvironment env):-初始化操作的方法,RoundEnvironment会提供很多有用的工具类Elements、Types和Filer等\u003Cbr\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003Eprocess(Set&? extends TypeElement& annoations, RoundEnvironment env):在这里进行注解的扫描,处理,Java代码的生成,是入口函数。注意!如果在此函数中,你生成了源代码,此process方法可能会被调用多次,因为你生成的源代码中可能也有会注解,process方法会继续对源代码文件进行处理,直至process中没有生成任何源文件。\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EgetSupportedAnnotationTypes:返回需要处理的注解类型,也就是说注解处理器只处理那些注解,这里是必须指定的。\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003EgetSupportedSourceVersion():用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cblockquote\u003E\u003Cp\u003E在Java 7中,你也可以使用注解来代替getSupportedAnnotationTypes()和getSupportedSourceVersion(),像这样:\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&text\&\u003E@SupportedSourceVersion(SourceVersion.latestSupported())\n@SupportedAnnotationTypes({\n
\u002F\u002F 合法注解全名的集合\n })\npublic class MyProcessor extends AbstractProcessor {\n\n
@Override\n
public synchronized void init(ProcessingEnvironment env){ }\n\n
@Override\n
public boolean process(Set&? extends TypeElement& annoations, RoundEnvironment env) { }\n}\n\u003C\u002Fcode\u003E\u003Cp\u003E因为兼容的原因,特别是针对Android平台,我建议使用重载getSupportedAnnotationTypes()和getSupportedSourceVersion()方法代替@SupportedAnnotationTypes和@SupportedSourceVersion。\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cul\u003E\u003Cli\u003E注册处理器 \u003C\u002Fli\u003E\u003C\u002Ful\u003E为了让自定义的Processor生效呢,需要在在processor的java同级目录新建resources\u002FMETA-INF\u002Fservices\u002Fjavax.annotation.processing.Processor文件\u003Cbr\u003E\u003Ccode lang=\&text\&\u003E
-processor\n
-ApolloAnnotationProcessor.java\n
- META-INF\n
- services\n
- javax.annotation.processing.Processor\n\u003C\u002Fcode\u003E\u003Cp\u003E然后在javax.annotation.processing.Processor文件中指定自定义的处理器,如:\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&text\&\u003Ecom.lsxiao.apollo.processor.ApolloAnnotationProcessor\n\u003C\u002Fcode\u003E\u003Cp\u003E多个处理器换行写。\u003C\u002Fp\u003E\u003Cp\u003E然后执行gradle build即可。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cp\u003E如果你在编译时注解处理中更高效率的开发,而不是用字符串拼接来生成源文件,就需要用到如下几个类库:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E\u003Ca href=\&https:\u002F\u002Flink.zhihu.com\u002F?target=https%3A\u002F\u002Fbitbucket.org\u002Fhvisser\u002Fandroid-apt\& class=\&\& data-editable=\&true\& data-title=\&android-apt\&\u003Eandroid-apt\u003C\u002Fa\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cbr\u003E\u003Cblockquote\u003E\u003Cp\u003EAndroid Studio原本是不支持注解处理器的, 但是用android-apt这个插件后, 我们就可以使用注解处理器了, \u003Cbr\u003E这个插件可以自动的帮你为生成的代码创建目录, 让生成的代码编译到APK里面去, 而且它还可以让最终编译出来的APK里面不包含注解处理器本身的代码, \u003Cbr\u003E\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E因为这部分代码只是编译的时候需要用来生成代码, 最终运行的时候是不需要的。 也就是说它主要有两个目的:\u003Cbr\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cul\u003E\u003Cli\u003E允许配置只在编译时作为注解处理器的依赖,而不添加到最后的APK或library\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cbr\u003E\u003Cp\u003E设置源路径,使注解处理器生成的代码能被Android Studio正确的引用那在什么情况下我们会需要使用它呢? \u003Cbr\u003E\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E当你需要引入Processor生成的源代码到你的代码中时。例如当你使用Dagger 2或AndroidAnnotaition. \u003Cbr\u003E\u003C\u002Fli\u003E\u003Cli\u003E该插件使得Android Studio可以配置生成资源的build path,避免IDE报错。 \u003Cbr\u003E\u003C\u002Fli\u003E\u003Cli\u003E当使用apt添加添加依赖,它将不会被包含到最终的APK里。\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003C\u002Fblockquote\u003E\u003Cbr\u003E\u003Cul\u003E\u003Cli\u003E\u003Ca href=\&https:\u002F\u002Flink.zhihu.com\u002F?target=https%3A\u002F\u002Fgithub.com\u002Fgoogle\u002Fauto\& class=\&\& data-editable=\&true\& data-title=\&Google Auto\&\u003EGoogle Auto\u003C\u002Fa\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cbr\u003E\u003Cblockquote\u003EGoogle Auto的主要作用是注解Processor类,并对其生成META-INF的配置信息, 可以让你不用去写META-INF这些配置文件,只要在自定义的Processor上面加上@AutoService(Processor.class)\u003C\u002Fblockquote\u003E\u003Cul\u003E\u003Cli\u003E\u003Ca href=\&https:\u002F\u002Flink.zhihu.com\u002F?target=https%3A\u002F\u002Fgithub.com\u002Fsquare\u002Fjavapoet\& class=\&\& data-editable=\&true\& data-title=\&Square javapoet\&\u003ESquare javapoet\u003C\u002Fa\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cbr\u003E\u003Cblockquote\u003Ejavapoet:A Java API for generating .java source files.可以更方便的生成代码,它可以帮助我们通过类调用的形式来生成代码。\u003Cbr\u003E\u003C\u002Fblockquote\u003E\u003Cbr\u003E\u003Cp\u003E\u003Cb\u003EApolloAnnotationProcessor.java\u003C\u002Fb\u003E\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&text\&\u003E@AutoService(Processor.class)\npublic class ApolloAnnotationProcessor extends AbstractProcessor {\n
private ReceiveAnnotationHandler mReceiveAnnotationH\n\n
\u002F\u002Finit():初始化操作的方法,RoundEnvironment会提供很多有用的工具类Elements、Types和Filer等。\n
@Override\n
public synchronized void init(ProcessingEnvironment processingEnv) {\n
super.init(processingEnv);\n
Filer mFiler = processingEnv.getFiler();\n
Types mTypeUtil = processingEnv.getTypeUtils();\n
Elements mElementUtil = processingEnv.getElementUtils();\n
Messager mMessager = processingEnv.getMessager();\n
BaseHandler.init(mMessager, mTypeUtil, mElementUtil, mFiler);\n
mReceiveAnnotationHandler = new ReceiveAnnotationHandler();\n
\u002F\u002Fprocess()相当于每个处理器的主函数main()。在该方法中去扫描、评估、处理以及生成Java文件。\n
@Override\n
public boolean process(Set&? extends TypeElement& annotations, RoundEnvironment roundEnv) {\n
mReceiveAnnotationHandler.process(roundEnv);\\n
\u002F**\n
* 指定该注解处理器需要处理的注解类型\n
* @return 需要处理的注解类型名的集合Set&String&\n
@Override\n
public Set&String& getSupportedAnnotationTypes() {\n
Set&String& types = new HashSet&&();\n
types.add(com.lsxiao.apllo.annotations.Receive.class.getCanonicalName());\n\n
\u002F**\n
* 指定使用的java版本。通常这里会直接放回SourceVersion.latestSupported()即可。\n
* @return SourceVersion\n
@Override\n
public SourceVersion getSupportedSourceVersion() {\n
return SourceVersion.latestSupported();\n
}\n}\n\u003C\u002Fcode\u003E\u003Cbr\u003E\u003Cp\u003E\u003Cb\u003EReceiveAnnotationHandler.java(把process里面解析Receive的任务抽离出来,方便以后增加注解后的扩展)\u003C\u002Fb\u003E\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&text\&\u003Epublic class ReceiveAnnotationHandler extends BaseHandler {\nprivate Map&DeclaredType, List&ExecutableElement&& mClassMethodMap = new HashMap&&();\n\u002F\u002F如果生成了新的源文件process()能够被调用多次,因为生成的源文件中可能会有注解,它们还将会被ApolloAnnotationProcessor处理。\n
\u002F\u002F所以这里需要是个是否完成标志变量,避免重复处理注解 创建源文件造成异常\n
private boolean handleComplete =\n\n@Override\n
public void process(RoundEnvironment roundEnv) {\nif (handleComplete) {\\n
}\n\n\u002F\u002F单例变量\n
FieldSpec.Builder fieldBuilder = FieldSpec.builder(Apollo.SubscriberBinder.class, \&sInstance\&, Modifier.PRIVATE, Modifier.STATIC);\n\n\u002F\u002F单例方法\n
MethodSpec.Builder instanceMethodBuilder = MethodSpec.methodBuilder(\&instance\&)\n
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.SYNCHRONIZED)\n
.returns(Apollo.SubscriberBinder.class)\n
.beginControlFlow(\&if (null == sInstance)\&)\n
.addStatement(\&sInstance = new SubscriberBinderImplement()\&)\n
.endControlFlow()\n
.addStatement(\&return sInstance\&);\n\u002F\u002F绑定方法\n
MethodSpec.Builder bindMethodBuilder = MethodSpec.methodBuilder(\&bind\&)\n
.addModifiers(Modifier.PUBLIC)\n
.addAnnotation(Override.class)\n
.returns(SubscriptionBinder.class)\n
.addParameter(Object.class, \&object\&)\n
.addStatement(\&final $T subscriptionBinder = new $T()\&, SubscriptionBinder.class, SubscriptionBinder.class);\n\nfor (Element element : roundEnv.getElementsAnnotatedWith(Receive.class)) {\n\n\u002F\u002F注解最最外侧必须是一个类\n
if (element.getEnclosingElement().getKind() != ElementKind.CLASS) {\n\u002F\u002F打印出错信息\n
error(\&@Receive must be wrapped by a class\&);\n
}\n\nif (element.getKind() != ElementKind.METHOD) {\n
error(\&@Receive only support method!\&);\n
}\n\n\u002F\u002F转行成可执行element\n
ExecutableElement receiveMethodElement = (ExecutableElement)\n\n\u002F\u002F找到注解所属的类\n
TypeElement classElementAnnotationIn = (TypeElement) element.getEnclosingElement();\n\n\u002F\u002F获取类的完全限定名\n
final DeclaredType declaredType = getTypeUtil().getDeclaredType(classElementAnnotationIn);\n\n
List&ExecutableElement& methodList = mClassMethodMap.get(declaredType);\nif (methodList == null) {\n
methodList = new ArrayList&&();\nmClassMethodMap.put(declaredType, methodList);\n
}\n\n\u002F\u002F存储方法\n
methodList.add(receiveMethodElement);\n
}\n\nfor (DeclaredType classTypeAnnotationIn : mClassMethodMap.keySet()) {\n
String receiveMethodInvoker = StrUtil.dot2Underline(classTypeAnnotationIn.toString());\n
bindMethodBuilder\n
.beginControlFlow(\&if(object.getClass().getCanonicalName().equals($S))\&, classTypeAnnotationIn.toString())\n
.addStatement(\&final $T $N=($T)object\&, classTypeAnnotationIn, receiveMethodInvoker, classTypeAnnotationIn);\nfor (ExecutableElement methodElement : mClassMethodMap.get(classTypeAnnotationIn)) {\n\n\u002F\u002Freceive方法只能有一个变量\n
if (methodElement.getParameters().size() & 1) {\n
error(\&the \& + methodElement.toString() + \& method in \& + classTypeAnnotationIn.toString() + \&
only support 1 parameter,but there are \& + methodElement.getParameters().size());\n
}\n\n\u002F\u002F获取方法第一个变量\n
VariableElement eventVariable = methodElement.getParameters().get(0);\n\n\u002F\u002F获取tag值\n
String tag = methodElement.getAnnotation(Receive.class).tag();\n\n
Receive.Thread observeOn = methodElement.getAnnotation(Receive.class).observeOn();\n\n
Receive.Thread subscribeOn = methodElement.getAnnotation(Receive.class).subscribeOn();\n\n\u002F\u002F获取receiveMethod是否接收sticky event\n
boolean isSticky = methodElement.getAnnotation(Receive.class).type() == Receive.Type.STICKY;\n\n
String receiveMethod = methodElement.getSimpleName().toString();\n
String onSubscribeMethod = isSticky ? \&toObservableSticky\& : \&toObservable\&;\n
String eventVariableClassType = eventVariable.asType().toString() + \&.class\&;\n
String eventVariableClass = eventVariable.asType().toString();\n
String eventVariableInstance = eventVariable.getSimpleName().toString().toLowerCase();\n\n
bindMethodBuilder\n
.addStatement(\&subscriptionBinder.add($T.get().$N($S,$N).subscribeOn($T.get().getThread().get($N.$N)).observeOn($T.get().getThread().get($N.$N)).subscribe(\& +\n\&new $T&$N&(){\& +\n\&@Override \& +\n\&public void call($N $N){\& +\n\&$N.$N($N);\& +\n\&}},\& +\n\&new $T&$T&(){\& +\n\&@Override \& +\n\&public void call($T a){\& +\n\&a.printStackTrace();\& +\n\&}}\& +\n\&))\&,\n
Apollo.class,\n
onSubscribeMethod,\n
eventVariableClassType,\n
Apollo.class,\n
Receive.Thread.class.getCanonicalName(),\n
subscribeOn.name(),\n
Apollo.class,\n
Receive.Thread.class.getCanonicalName(),\n
observeOn.name(),\n
Action1.class,\n
eventVariableClass,\n
eventVariableClass,\n
eventVariableInstance,\n
receiveMethodInvoker,\n
receiveMethod,\n
eventVariableInstance,\n
Action1.class,\n
Throwable.class,\n
Throwable.class);\n
bindMethodBuilder.endControlFlow();\n
bindMethodBuilder.addStatement(\&return subscriptionBinder\&);\n\n
TypeSpec subscriberClass = TypeSpec.classBuilder(\&SubscriberBinderImplement\&)\n
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)\n
.addSuperinterface(Apollo.SubscriberBinder.class)\n
.addField(fieldBuilder.build())\n
.addMethod(instanceMethodBuilder.build())\n
.addMethod(bindMethodBuilder.build())\n
.build();\n\n
generateCode(subscriberClass);\n
}\n\nprivate void generateCode(TypeSpec subscriberClass) {\n
JavaFile javaFile = JavaFile.builder(\&com.lsxiao.apollo.generate\&, subscriberClass)\n
.build();\ntry {\n
javaFile.writeTo(getFiler());\nhandleComplete =\n
} catch (IOException e) {\n
e.printStackTrace();\n
}\n}\n\u003C\u002Fcode\u003E\u003Cul\u003E\u003Cli\u003E上面的代码使用Javapoet完成了一个SubscriberBinder接口的实现类,提供了静态方法能够获取SubscriberBinderImplement单例。\u003Cbr\u003E\u003C\u002Fli\u003E\u003Cli\u003E被注解的方法只能接收一个事件参数\u003C\u002Fli\u003E\u003Cli\u003E被注解的方法最外侧结点必须是一个类\u003C\u002Fli\u003E\u003Cli\u003E根据注解的tag,type,以及thread分别为对应的方法订阅对应的tag事件,并且在输出的时候讲事件转换成发送时候的类型,最后在subscribeOn和observeO指定线程调度器。\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cp\u003E最后demo中生成的源文件如下\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&text\&\u003Epublic final class SubscriberBinderImplement implements Apollo.SubscriberBinder {\nprivate static Apollo.SubscriberBinder sI\n\npublic static synchronized Apollo.SubscriberBinder instance() {\nif (null == sInstance) {\nsInstance = new SubscriberBinderImplement();\n
}\nreturn sI\n
}\n\n@Override\n
public SubscriptionBinder bind(Object object) {\nfinal SubscriptionBinder subscriptionBinder = new SubscriptionBinder();\nif (object.getClass().getCanonicalName().equals(\&com.lsxiao.apollo.demo.activity.MainActivity\&)) {\nfinal MainActivity com_lsxiao_apollo_demo_activity_MainActivity = (MainActivity)\n
subscriptionBinder.add(Apollo.get().toObservable(\&event_show_book\&, com.lsxiao.apollo.demo.activity.MainActivity.Book.class).subscribeOn(Apollo.get().getThread().get(com.lsxiao.apllo.annotations.Receive.Thread.IO)).observeOn(Apollo.get().getThread().get(com.lsxiao.apllo.annotations.Receive.Thread.MAIN)).subscribe(new Action1&com.lsxiao.apollo.demo.activity.MainActivity.Book&() {\n@Override\n
public void call(com.lsxiao.apollo.demo.activity.MainActivity.Book book) {\ncom_lsxiao_apollo_demo_activity_MainActivity.receiveBook(book);\n
}, new Action1&Throwable&() {\n@Override\n
public void call(Throwable a) {\n
a.printStackTrace();\n
subscriptionBinder.add(Apollo.get().toObservable(\&event_show_user\&, com.lsxiao.apollo.demo.activity.MainActivity.User.class).subscribeOn(Apollo.get().getThread().get(com.lsxiao.apllo.annotations.Receive.Thread.IO)).observeOn(Apollo.get().getThread().get(com.lsxiao.apllo.annotations.Receive.Thread.MAIN)).subscribe(new Action1&com.lsxiao.apollo.demo.activity.MainActivity.User&() {\n@Override\n
public void call(com.lsxiao.apollo.demo.activity.MainActivity.User user) {\ncom_lsxiao_apollo_demo_activity_MainActivity.receiveUser(user);\n
}, new Action1&Throwable&() {\n@Override\n
public void call(Throwable a) {\n
a.printStackTrace();\n
}\nif (object.getClass().getCanonicalName().equals(\&com.lsxiao.apollo.demo.activity.BookActivity\&)) {\nfinal BookActivity com_lsxiao_apollo_demo_activity_BookActivity = (BookActivity)\n
subscriptionBinder.add(Apollo.get().toObservableSticky(\&event_show_book\&, com.lsxiao.apollo.demo.activity.MainActivity.Book.class).subscribeOn(Apollo.get().getThread().get(com.lsxiao.apllo.annotations.Receive.Thread.IO)).observeOn(Apollo.get().getThread().get(com.lsxiao.apllo.annotations.Receive.Thread.MAIN)).subscribe(new Action1&com.lsxiao.apollo.demo.activity.MainActivity.Book&() {\n@Override\n
public void call(com.lsxiao.apollo.demo.activity.MainActivity.Book book) {\ncom_lsxiao_apollo_demo_activity_BookActivity.receiveBook(book);\n
}, new Action1&Throwable&() {\n@Override\n
public void call(Throwable a) {\n
a.printStackTrace();\n
}\nreturn subscriptionB\n
}\n}\n\u003C\u002Fcode\u003E\u003Cbr\u003E\u003Cul\u003E\u003Cli\u003E\u003Cb\u003E初始化\u003C\u002Fb\u003E\u003Cbr\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cbr\u003E\u003Ccode lang=\&text\&\u003Epublic class App extends Application {\n@Override\n
public void onCreate() {\nsuper.onCreate();\n
Apollo.get().init(SubscriberBinderImplement.instance(), AndroidSchedulers.mainThread());\n
}\n}\n\u003C\u002Fcode\u003E以上就是整个Complie-time RxBus的整体实现,详细细节可以查看\u003Ca href=\&https:\u002F\u002Flink.zhihu.com\u002F?target=https%3A\u002F\u002Fgithub.com\u002Flsxiao\u002FApollo\& class=\&\& data-editable=\&true\& data-title=\&Apollo\&\u003EApollo\u003C\u002Fa\u003E。\u003Cbr\u003E\u003Cp\u003Eps.注意!再次提醒目前 \u003Ca href=\&https:\u002F\u002Flink.zhihu.com\u002F?target=https%3A\u002F\u002Fgithub.com\u002Flsxiao\u002FApollo\& class=\&\& data-editable=\&true\& data-title=\&Apollo\&\u003EApollo\u003C\u002Fa\u003E 仍处于非稳定版本,0.2版本之前不建议用于生产环境。\u003C\u002Fp\u003E&,&updated&:new Date(&T20:37:25.000Z&),&canComment&:false,&commentPermission&:&anyone&,&commentCount&:5,&likeCount&:36,&state&:&published&,&isLiked&:false,&slug&:&&,&isTitleImageFullScreen&:true,&rating&:&none&,&sourceUrl&:&&,&publishedTime&:&T04:37:25+08:00&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&url&:&\u002Fp\u002F&,&titleImage&:&https:\u002F\u002Fpic3.zhimg.com\u002Fv2-9e25eab0a7e80ffb4889_r.jpg&,&summary&:&&,&href&:&\u002Fapi\u002Fposts\u002F&,&meta&:{&previous&:null,&next&:null},&snapshotUrl&:&&,&commentsCount&:5,&likesCount&:36},&&:{&title&:&保存\u002F恢复Activity和Fragment状态的最佳实践(译)&,&author&:&lsxiao&,&content&:&\u003Cp\u003E\u003Cu\u003E很久之前看过的一篇文章,写的很好。\u003C\u002Fu\u003E\u003Cu\u003E转载并译于:\u003Ca href=\&https:\u002F\u002Finthecheesefactory.com\u002Fblog\u002Ffragment-state-saving-best-practices\u002Fen\& data-editable=\&true\& data-title=\&inthecheesefactory.com 的页面\& class=\&\&\u003Ehttps:\u002F\u002Finthecheesefactory.com\u002Fblog\u002Ffragment-state-saving-best-practices\u002Fen\u003C\u002Fa\u003E。\u003C\u002Fu\u003E\u003C\u002Fp\u003E\u003Cp\u003E\u003Cb\u003E\u003Cu\u003E由于知乎不支持GIF动图,请查看原文动图。\u003C\u002Fu\u003E\u003C\u002Fb\u003E\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cb\u003EAuthor: nuuneoi (Android GDE, CTO & CEO at The Cheese Factory)\u003Cbr\u003E\u003C\u002Fb\u003E\u003Cbr\u003E\u003Cb\u003EA full-stack developer with more than 6 years experience on Android Application Development and more than 12 years in Mobile Application Development industry. Also has skill in Infrastucture, Service Side, Design, UI&UX, Hardware, Optimization, Cooking, Photographing, Blogging, Training, Public Speaking and do love to share things to people in the world!\u003C\u002Fb\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E几个月以前,我发布了一篇关于Fragment状态保存和恢复的文章,那可能是目前为止最好的方式用于保存\u002F恢复 Android Fragment 的状态。我收到了很多来自世界各地的Android开发者有价值的反馈。十分感谢你们 =)\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E无论如何,StatedFragment打破了设计模式,我使用了不同于Android状态保存\u002F恢复的方式来设计它,这样做的目的是为了让Android开发者能够更简单的理解Fragment状态的保存与恢复,就像Activity做的一样(同时处理View状态和实例状态)。\u003C\u002Fp\u003E\u003Cp\u003E所以我通过开发StatedFragment做了一个实验,并且看看它是怎么做的,看看StatedFragment这样的设计是否更容易被理解?是否对开发者更加友好?\u003C\u002Fp\u003E\u003Cp\u003E现在,两个月的实验过去了,我相信我已经得到了结果。虽然 StatedFragment 有点容易被理解,但是它同时带来了很大的问题。它破坏了Android View的基本框架.所以我认为它这是很糟糕的,可能会导致长远的影响。事实上我已经对自己的代码感到担忧了... 由于这个原因,\u003Cstrong\u003E我决定从现在开始弃用StatedFragment\u003C\u002Fstrong\u003E。并且,为了弥补我错误,我写了这篇文章,用可见动图的方式来展示基于Android的设计如何保存和恢复Fragment的状态的最佳实践。\u003C\u002Fp\u003E\u003Ch1\u003E\u003Cb\u003E理解在Activity的状态被保存\u002F恢复的时候发生了什么\u003C\u002Fb\u003E\u003C\u002Fh1\u003E\u003Cp\u003E当Activity的\u003Cu\u003EonSaveInstanceState\u003C\u002Fu\u003E被调用的时候,Activity将会从View 层次(View Hierachy)中的每一个View中自动搜集View的状态。请注意,只会搜集实现了View状态保存\u002F恢复的内部方法的View的数据。一旦\u003Cu\u003EonRestoreInstanceState\u003C\u002Fu\u003E被调用,Activity将会将这些搜集到的数据一对一的返还给View 层次里在搜集的时候提供了同样的android:id属性的View。\u003C\u002Fp\u003E\u003Cp\u003E让我们看看。\u003C\u002Fp\u003E\u003Cimg src=\&6bb036e9d55f72b57bdebd2a4df7d323.jpg\& data-rawwidth=\&800\& data-rawheight=\&500\&\u003E\u003Cimg src=\&693d9c83b3acade59d7a9d84dbda0724.jpg\& data-rawwidth=\&800\& data-rawheight=\&500\&\u003E\u003Cp\u003E这就是为什么尽管Activity已经被销毁,而我们并没有做一些特别的事情来保存状态,但是EditText中键入的文本仍然能够呈现的原因。这并不是什么魔法,这些View 的状态已经被自动的保存和恢复回来了。\u003C\u002Fp\u003E\u003Cp\u003E这也是为什么View 在没有被设置\u003Cu\u003Eandroid:id\u003C\u002Fu\u003E属性的时候不能保存和恢复自己的状态的原因。\u003C\u002Fp\u003E\u003Cp\u003E尽管这些View 的状态被自动的保存了,但是Activity的成员变量并不会有同样的效果。这些成员变量会被和Activity一起销毁。你可以手动的保存和恢复它们,通过\u003Cu\u003EonSaveInstanceState\u003C\u002Fu\u003E和\u003Cu\u003EonRestoreInstanceState\u003C\u002Fu\u003E方法。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&java\&\u003Epublic class MainActivity extends AppCompatActivity {\n \n
\u002F\u002F These variable are destroyed along with Activity\n
private int someVarA;\n
private String someVarB;\n \n\n
@Override\n
protected void onSaveInstanceState(Bundle outState) {\n
super.onSaveInstanceState(outState);\n
outState.putInt(\&someVarA\&, someVarA);\n
outState.putString(\&someVarB\&, someVarB);\n
@Override\n
protected void onRestoreInstanceState(Bundle savedInstanceState) {\n
super.onRestoreInstanceState(savedInstanceState);\n
someVarA = savedInstanceState.getInt(\&someVarA\&);\n
someVarB = savedInstanceState.getString(\&someVarB\&);\n
}\n \n}\u003C\u002Fcode\u003E\u003Cp\u003E这就是为了恢复Activity实例的状态和View 状态需要做的。\u003C\u002Fp\u003E\u003Ch1\u003E\u003Cb\u003E理解在Fragment的状态被保存\u002F恢复的时候发生了什么\u003C\u002Fb\u003E\u003C\u002Fh1\u003E\u003Cp\u003E如果Fragment被系统销毁,所有事情都会发生的像Activity发生的那样。\u003C\u002Fp\u003E\u003Cimg src=\&e92df91fdbafd2ed6177d7c.jpg\& data-rawwidth=\&800\& data-rawheight=\&500\&\u003E\u003Cimg src=\&c2fe5b0fccae0.jpg\& data-rawwidth=\&800\& data-rawheight=\&500\&\u003E\u003Cp\u003E这意味着每一个单独的成员变量被销毁了。你必须分别地通过\u003Cu\u003EonSaveInstanceState\u003C\u002Fu\u003E和\u003Cu\u003EonActivityCreated\u003C\u002Fu\u003E方法,手动的保存和恢复这些变量。 请注意在Fragment里面没有\u003Cu\u003EonRestoreInstanceState\u003C\u002Fu\u003E方法存在。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&java\&\u003Epublic class MainFragment extends Fragment {\n \n
\u002F\u002F These variable are destroyed along with Activity\n
private int someVarA;\n
private String someVarB;\n \n\n
@Override\n
public void onSaveInstanceState(Bundle outState) {\n
super.onSaveInstanceState(outState);\n
outState.putInt(\&someVarA\&, someVarA);\n
outState.putString(\&someVarB\&, someVarB);\n
@Override\n
public void onActivityCreated(@Nullable Bundle savedInstanceState) {\n
super.onActivityCreated(savedInstanceState);\n
someVarA = savedInstanceState.getInt(\&someVarA\&);\n
someVarB = savedInstanceState.getString(\&someVarB\&);\n
}\n \n}\u003C\u002Fcode\u003E\u003Cp\u003E对于Fragment来说,有一些特殊情况不同于Activity,我觉得你需要知道这些情况。\u003Cstrong\u003E一旦Fragment从后退栈中返回,它的View 会被销毁,并重新创建\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cimg src=\&5cea2c97c5b.png\& data-rawwidth=\&317\& data-rawheight=\&847\&\u003E\u003Cp\u003E\u003Cstrong\u003E这这种情况下,Fragment并不会被销毁,只有Fragment中的View 会被销毁。\u003C\u002Fstrong\u003E结果是,并不会发生任何实例状态的保存。那上面展示在Fragment生命周期重新创建View的时候发生了什么?\u003C\u002Fp\u003E\u003Cp\u003E别惊讶,因为Android是这样设计的。在这种情况下,Fragment中的View 状态的保存\u002F恢复会被内部调用。结果就是,每一个实现了内部View 状态保存\u002F恢复的View ,将会被自动的保存并且恢复状态,例如带有\u003Cu\u003Eandroid:freezesText=\&true\&\u003C\u002Fu\u003E属性的EditText或者TextView。就像之前完美显示的一样。\u003C\u002Fp\u003E\u003Cimg src=\&b0cfe2ddaba4522fef9c50bf.jpg\& data-rawwidth=\&800\& data-rawheight=\&500\&\u003E\u003Cp\u003E请注意,在这种情况下,只有View 被销毁(并重建)了。Fragment不会被销毁,就像它内部的成员变量一样。所以你不需要对它们做任何事情,不需要任何额外的代码。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&text\&\u003Epublic class MainFragment extends Fragment {\n \n
\u002F\u002F These variable still persist in this case\n
private int someVarA;\n
private String someVarB;\n \n\n
...\n \n}\u003C\u002Fcode\u003E\u003Cp\u003E你也许已经注意到了,如果Fragment中的每一个View 都在内部实现了View 的状态的保存和恢复。在这种情况下,你就没有必要做任何事情,View 状态会被自动的恢复,并且Fragment中的成员变量也和之前的一样。\u003C\u002Fp\u003E\u003Cp\u003E所以Fragment状态保存\u002F恢复最佳实践的第一条件就是...\u003C\u002Fp\u003E\u003Ch2\u003E\u003Cb\u003E应用中使用的每一个单独的View都必须在内部实现状态的保存和恢复\u003C\u002Fb\u003E\u003C\u002Fh2\u003E\u003Cp\u003EAndroid内部通过onSaveInstanceState 和 onRestoreInstanceState 方法提供了保存和恢复View 状态的机制。开发者的任务就是实现它。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&java\&\u003Epublic class CustomView extends View {\n \n
@Override\n
public Parcelable onSaveInstanceState() {\n
Bundle bundle = new Bundle();\n
\u002F\u002F Save current View's state here\n \n
@Override\n\n
public void onRestoreInstanceState(Parcelable state) {\n
super.onRestoreInstanceState(state);\n
\u002F\u002F Restore View's state here\n
...\n\n \n}\u003C\u002Fcode\u003E\u003Cp\u003E基本上来说,每一个单独的Android提供的的标准View 组件都已经在内部完成了这些事情,例如EditText,TextView,Checkbox. 尽管你需要手动让它生效,比如,你需要为TextView设置\u003Cu\u003Eandroid:freezeText\u003C\u002Fu\u003E为true,来使用这个功能。\u003C\u002Fp\u003E\u003Cp\u003E但是如果我们讨论关于互联网上贡献的第三方的自定义组件。我必须说,它们大多数都没有实现这部分的代码,这在使用中会导致很大的问题。\u003C\u002Fp\u003E\u003Cp\u003E如果你决定使用第三方的自定义组件,你不得不确保它已经在内部实现了View 的保存和恢复,否则你必须创建继承这个三方组件的一个派生的子类,并且自己实现\u003Cu\u003EonSaveInstanceState\u003C\u002Fu\u003E和\u003Cu\u003EonRestoreInstanceState\u003C\u002Fu\u003E方法。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&java\&\u003E\u002F\u002F Assumes that SomeSmartButton is a 3rd Party view that\n\u002F\u002F View State Saving\u002FRestoring are not implemented internally\n\u002F\u002F\npublic class SomeBetterSmartButton extends SomeSmartButton {\n \n\n
@Override\n
public Parcelable onSaveInstanceState() {\n
Bundle bundle = new Bundle();\n
\u002F\u002F Save current View's state here\n \n
@Override\n
public void onRestoreInstanceState(Parcelable state) {\n
super.onRestoreInstanceState(state);\n
\u002F\u002F Restore View's state here\n
...\n \n}\u003C\u002Fcode\u003E\u003Cp\u003E并且,如果你创建你自己的自定义View或者自定义ViewGroup,也不要忘记实现这两个方法。应用中每一个类型的View实现这部分都是很重要的。\u003C\u002Fp\u003E\u003Cp\u003E还有,不要忘记为每一个你需要开启View状态保存和恢复的View设置android:id属性,不然它们的状态不能正确的恢复。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&java\&\u003E&EditText\n
android:id=\&@+id\u002FeditText1\&\n
android:layout_width=\&match_parent\&\n
android:layout_height=\&wrap_content\& \u002F&\n \n&EditText\n
android:id=\&@+id\u002FeditText2\&\n
android:layout_width=\&match_parent\&\n
android:layout_height=\&wrap_content\& \u002F&\n \n&CheckBox\n
android:id=\&@+id\u002FcbAgree\&\n
android:text=\&I agree\&\n
android:layout_width=\&wrap_content\&\n
android:layout_height=\&wrap_content\& \u002F&\n\n\u003C\u002Fcode\u003E\u003Cp\u003E这篇文章已经进行到了一半!\u003C\u002Fp\u003E\u003Ch1\u003E\u003Cb\u003E完全分开处理Fragment状态和view状态\u003C\u002Fb\u003E\u003C\u002Fh1\u003E\u003Cp\u003E为了使你的代码变得干净和可扩展,你最好把Fragment状态和View状态分开处理。如果这里有任何属性是属于View的,在View内部进行保存和恢复.如果这里有任何属性是属于Fragment的,在Fragment内部进行保存和恢复。这里有一个例子。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Ccode lang=\&java\&\u003Epublic class MainFragment extends Fragment {\n \n
private String dataGotFromS\n \n\n
@Override\n
public void onSaveInstanceState(Bundle outState) {\n
super.onSaveInstanceState(outState);\n
outState.putString(\&dataGotFromServer\&, dataGotFromServer);\n
@Override\n
public void onActivityCreated(Bundle savedInstanceState) {\n
super.onActivityCreated(savedInstanceState);\n
dataGotFromServer = savedInstanceState.getString(\&dataGotFromServer\&);\n
...\n \n}\u003C\u002Fcode\u003E\u003Cp\u003E\u003Cstrong\u003E\u003Cu\u003E让我再重复一遍\u003C\u002Fu\u003E\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cp\u003E\u003Cstrong\u003E\u003Cu\u003E不要在Fragment中的 onSaveInstanceState 中保存View状态!!!\u003C\u002Fu\u003E\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cp\u003E\u003Cb\u003E\u003Cu\u003E不要在Fragment中的 onSaveInstanceState 中保存View状态!!!\u003C\u002Fu\u003E\u003Cbr\u003E\u003C\u002Fb\u003E\u003C\u002Fp\u003E\u003Cp\u003E\u003Cu\u003E\u003Cb\u003E不要在Fragment中的 onSaveInstanceState 中保存View状态!!!\u003C\u002Fb\u003E\u003C\u002Fu\u003E\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E\u003Cstrong\u003E\u003Cu\u003E反之亦然。\u003C\u002Fu\u003E\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cp\u003E以上就是本篇文章所有的内容了,是一篇关于如何保存和恢复Activity,Fragment和View状态的最佳实践,希望你在这篇文章中找到一些有用的信息=)\u003C\u002Fp\u003E\u003Ch1\u003E\u003Cb\u003E再见StatedFragment,迎接NestedActivityResultFragment\u003C\u002Fb\u003E\u003C\u002Fh1\u003E\u003Cp\u003E请按照上面所描述的方法来保存恢复Activity,Fragment,View的状态。从现在开始,让我把StatedFragment废弃掉吧。\u003C\u002Fp\u003E\u003Cp\u003E还有,StatedFragment中获取\u003Cu\u003EonActivityResult\u003C\u002Fu\u003E的特性在NestedFragment中仍然是可以使用的。为了避免未来的混淆,我决定把这个功能分离到NestedActivityResultFragment这个新的类里面,在v0.10.0以及后续的版本可用。\u003C\u002Fp\u003E\u003Cp\u003E更多的关于它的信息参见 \u003Ca href=\&https:\u002F\u002Fgithub.com\u002Fnuuneoi\u002FStatedFragment\& data-editable=\&true\& data-title=\&https:\u002F\u002Fgithub.com\u002Fnuuneoi\u002FStatedFragment\& class=\&\&\u003Ehttps:\u002F\u002Fgithub.com\u002Fnuuneoi\u002FStatedFragment\u003C\u002Fa\u003E
,请随时查看。\u003C\u002Fp\u003E\u003Cp\u003E希望这篇文章中这种动态图片的方式能够帮助你们清楚的理解Activity,Fragment,View状态的恢复。对于前一篇文章的导致的混淆,我感到十分抱歉。^^\&\u003C\u002Fp\u003E&,&updated&:new Date(&T07:30:03.000Z&),&canComment&:false,&commentPermission&:&anyone&,&commentCount&:14,&likeCount&:230,&state&:&published&,&isLiked&:false,&slug&:&&,&isTitleImageFullScreen&:true,&rating&:&none&,&sourceUrl&:&&,&publishedTime&:&T15:30:03+08:00&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&url&:&\u002Fp\u002F&,&titleImage&:&https:\u002F\u002Fpic1.zhimg.com\u002Fv2-34d65eb3e3cb2e34cf71fc70_r.jpg&,&summary&:&&,&href&:&\u002Fapi\u002Fposts\u002F&,&meta&:{&previous&:null,&next&:null},&snapshotUrl&:&&,&commentsCount&:14,&likesCount&:230},&&:{&title&:&从零实现Lumen-JWT扩展包(序):前因&,&author&:&lsxiao&,&content&:&\u003Cp\u003E最近这段时间我寻思着把几个月前爬下来的6万多首诗词曲文做成一个API,免费开放给大家用。\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E这么做的原因有三:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E其一,是因为之前写过的古诗词文网由于有很多BUG,而且又在备案gushiciwen.com的域名,索性直接把网站下线,域名转回国内后,这段时间又忙,没时间弄阿里云的主机,闲置了快个多月了。\u003Cbr\u003E\u003C\u002Fli\u003E\u003Cli\u003E其二,是想给古诗词文网写一个Android版的开源示例的app,网站有了,但是我没有现成的REST API。\u003Cbr\u003E\u003C\u002Fli\u003E\u003Cli\u003E其三,是因为最近 \u003Ca href=\&https:\u002F\u002Fgithub.com\u002Ftaylorotwell\& class=\&\& data-editable=\&true\& data-title=\&taylorotwell\&\u003Etaylorotwell\u003C\u002Fa\u003E 发布了Lumen5.3,从使用Laravel以来,还没试过Lumen,所以先顺便尝试下Lumen。\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E说写就写吧,前几天就开工了,用了的Dingo\u002Fapi和tymondesigns\u002Fjwt-auth来做,不过过程中发现很多坑,而且这些坑大部分都需要自己去填,这样写下来,真的是很不爽。\u003C\u002Fp\u003E\u003Cp\u003E我真的不是想贬低tymondesigns\u002Fjwt-auth,这是一个很好的扩展包,提高了效率。但是,真的很不好用,我不知道作者是不是真的用过这个库实现过一些API的Demo,我就随便举个栗子,比如说客户端请求并发的问题。\u003C\u002Fp\u003E\u003Cp\u003E说这个问题前,先普及一些概念。\u003C\u002Fp\u003E\u003Cp\u003E由于JWT推荐Token过期时效尽量不要太长,比如1至2小时都是可以的,那么为了尽量减轻客户端主动请求刷新Token的操作,可以在每次客户端发起需要验证Token的请求时,对旧Token进行刷新,新Token会以 Authorization: \u003Cb\u003EBearer &token& \u003C\u002Fb\u003E头的形式添加到HTTP响应头中,因为开启了黑名单,旧的Token会在刷新完成后,被加入黑名单,也就不能再次使用了。\u003C\u002Fp\u003E\u003Cp\u003E当客户端请求并发的时候,问题就来了,请求的处理完成速度肯定会有先后,当前一个请求处理完成后,Token已经刷新了,后一个请求拿着一个已经被加入黑名单的Token问服务器要数据,此时请求只会被拒绝。\u003C\u002Fp\u003E\u003Cp\u003E如下所示,并发请求:\u003C\u002Fp\u003E\u003Cimg src=\&v2-e1c1c87b7d33b42c41fd0.png\& data-rawwidth=\&646\& data-rawheight=\&466\&\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cp\u003E用第三方包图的就是方便,结果还要去翻Issues,看看别人是怎么用的,着实恼火。\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E不过最新的1.0dev版本增加了JWT_BLACKLIST_GRACE_PERIOD配置,设置一个黑名单宽限时间,允许在遇到并发请求的时候,避免上述的情况,不过这个方案我认为不能根本上的解决并发的问题,而且我个人已经不想在花时间给未来可能带来的问题上了。\u003C\u002Fp\u003E\u003Cp\u003E所以,你们也就知道我要干什么了,别人的轮子不好用,不顺手,只有自己撸轮子了。\u003C\u002Fp\u003E\u003Cp\u003E这篇教程我会分为三个部分来写:\u003C\u002Fp\u003E\u003Col\u003E\u003Cli\u003E准备步骤,包括从如何设置命名空间,到创建ServiceProvider,以及如何让Lumen读取扩展包配置文件,简单测试是否能够读取配置等。\u003Cbr\u003E\u003C\u002Fli\u003E\u003Cli\u003E实现JWT,包括生成Token,校验Token,刷新Token,黑名单等,不过这部分我会根据自己的喜好进行设计。\u003C\u002Fli\u003E\u003Cli\u003E为Lumen集成JWT用户认证功能。\u003C\u002Fli\u003E\u003Cli\u003E发布扩展包到Packagist,让别人能够通过Composer轻松的把我们的扩展包集成到自己的项目中去。\u003Cbr\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003Cbr\u003E\u003Cp\u003E第一部分的代码在我写这篇序的时候已经完成了。\u003C\u002Fp\u003E\u003Cp\u003E最后每个部分的代码我都会以Tutorial-1、Tutorial-2、Tutorial-3、Tutorial-4、Tutorial-5分支的形式上传到Github中。\u003C\u002Fp\u003E\u003Cp\u003Emaster分支会存放完成后的扩展包代码。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cul\u003E\u003Cli\u003E\u003Ca href=\&https:\u002F\u002Fzhuanlan.zhihu.com\u002Fp\u002F2Fedit\& data-editable=\&true\& data-title=\&从零实现Lumen-JWT扩展包(序):前因\& class=\&\&\u003E从零实现Lumen-JWT扩展包(序):前因\u003C\u002Fa\u003E\u003Cbr\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Ca href=\&https:\u002F\u002Fzhuanlan.zhihu.com\u002Fp\u002F?refer=lsxiao\& class=\&\& data-editable=\&true\& data-title=\&从零实现Lumen-JWT扩展包(一):基础构建\&\u003E从零实现Lumen-JWT扩展包(一):基础构建\u003C\u002Fa\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Ca href=\&https:\u002F\u002Fzhuanlan.zhihu.com\u002Fp\u002F\& data-title=\&从零实现Lumen-JWT扩展包(二):JWT生成\& class=\&\& data-editable=\&true\&\u003E从零实现Lumen-JWT扩展包(二):JWT生成\u003C\u002Fa\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Ca href=\&https:\u002F\u002Fzhuanlan.zhihu.com\u002Fp\u002F\& data-editable=\&true\& data-title=\&从零实现Lumen-JWT扩展包(三):JWT解析\& class=\&\&\u003E从零实现Lumen-JWT扩展包(三):JWT解析\u003C\u002Fa\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Ca href=\&https:\u002F\u002Fzhuanlan.zhihu.com\u002Fp\u002F?refer=lsxiao\& data-editable=\&true\& data-title=\&从零实现Lumen-JWT扩展包(四):用户认证\&\u003E从零实现Lumen-JWT扩展包(四):用户认证\u003C\u002Fa\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Ca href=\&https:\u002F\u002Fzhuanlan.zhihu.com\u002Fp\u002F?refer=lsxiao\& data-editable=\&true\& data-title=\&从零实现Lumen-JWT扩展包(五):发布扩展包\&\u003E从零实现Lumen-JWT扩展包(五):发布扩展包\u003C\u002Fa\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cbr\u003E\u0

我要回帖

更多关于 化妆品排行榜前十名 的文章

 

随机推荐