-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.json
More file actions
1 lines (1 loc) · 61.6 KB
/
content.json
File metadata and controls
1 lines (1 loc) · 61.6 KB
1
[{"title":"《Clean Architecture》读书笔记 - 业务逻辑","date":"2019-07-28T15:45:46.000Z","path":"2019/07/28/clean-architecture-business-logic/","text":"背景读了 Martin大叔的 《Clean Architecture》,对其中第20章 业务逻辑 ,记录下自己的理解。 业务逻辑 业务逻辑就是程序中那些真正用于赚钱或省钱的业务逻辑与过程。 如我做的直播类应用,用户送礼给主播,然后直播间播放礼物特效。这就是业务赚钱的一个核心逻辑。这就叫“关键业务逻辑”。这个过程中处理的一些数据,如礼物id,数量,价格,收礼对象等,就是“关键业务数据”。这两者是紧密相关的,很适合放在同一个对象中处理,这种对象就称为“业务实体(Entity)”。 业务实体 业务实体这种对象中包含了一系列用于操作关键数据的业务逻辑。这些实体对象要么直接包含关键数据,要么可以很容易地访问这些数据。业务实体的接口层则是由那些实现关键业务逻辑、操作关键数据的函数组成的。 如 频道 这个业务实体,它内部有频道信息,频道状态等关键数据,同时提供了进频道,离开频道,查询频道信息,频道状态等接口。 这个类独自代表了整个业务逻辑,它与数据库、用户界面、第三方框架等内容无关。该类可以在任何一个系统中提供与其业务逻辑相关的服务,它不会去管这个系统是如何呈现给用户的,数据是如何存储的,或是以何种方式运行的。总而言之,业务实体这个概念中应该只有业务逻辑,没有别的。 业务实体是实现自己的业务逻辑,依赖到的其他细节,如数据存储、网络通信、界面等,都应该声明好接口,由外部提供具体实现。这样的业务实体类是很稳定的,只与业务逻辑有关,与其他细节都无关。具备了很好的复用性,跨平台性。如我经历过的一个项目,它的一个核心玩法的逻辑层,我们就在多个app中进行了复用。因为不同平台下不同app宿主下,这个玩法的业务逻辑都是一样的,是稳定的,所以能够复用。 业务实体这个概念只要求我们将关键业务数据和关键业务逻辑绑定在同一个独立的软件模块内。 业务实体并不是专指一个面向对象编程语言中的一个类。它是一个概念,只要是一个独立的软件模块即可。 用例 UseCase用例是什么? 用例本质上就是关于如何操作一个自动化系统的描述,它定义了用户需要提供的输入数据、用户应该得到的输出信息以及产生输出所应该采取的处理步骤。当然,用例所描述的是某种特定应用情景下的业务逻辑,它并非业务实体中所包含的关键业务逻辑。 martin在这里举了一个银行为新贷款收集客户联系信息的用例,该用例有输入,输出,还定义了该情景下的业务逻辑。其中还调用到了关键业务实体“客户”,这里的客户就是一个业务实体,其中包含了处理银行与客户之间关系的关键业务逻辑。 用例中包含了对如何调用业务实体中的关键业务逻辑的定义。简而言之,用例控制着业务实体之间的交互方式。 我还是以直播app为例,“频道”是一个业务实体,“主播开播”我觉得就可以认为是一个用例。 输入是主播提供的开播标题、封面等, 输出是进入一个频道内(若首次开播,可能会先创建该频道),然后开始音视频推流直播。 该情景下的业务逻辑可能是:检查输入信息合法;检查频道是否存在,新建or进入频道;进入成功后,通知媒体系统开始推流;这个用例里就控制了“频道”、“媒体系统”等实体的交互,共同完成了开播这个用例的功能。 用例包含什么,不包含什么 用例并不描述系统与用户之间的接口,它只描述该应用在某些特定情景下的业务逻辑,这些业务逻辑所规范的是用户与业务实体之间的交互方式,它与数据流入/流出系统的方式无关。 所以我们只看用例,是没有办法看出系统是在什么平台交付的,如web,手机,或者是命令行模式。即用例不包括数据输入、输出和具体系统平台的接口描述。 用例对象中包含了一个或多个实现了特定应用情景的业务逻辑函数。当然除此之外,用例对象中也包含了输入数据、输出数据以及相关业务实体的引用,以方便调用。 这就是用例应该包含的内容。 实体和用例的依赖关系 业务实体这样的高层概念是无须了解像用例这样的低层概念的。反之,低层的业务用例却需要了解高层的业务实体。那么,为什么业务实体属于高层概念,而用例输入低层概念呢?因为用例描述的是一个特定的应用情景,这样一来,用例必然会更靠近系统的输入和输出。而业务实体是一个可以适用于多个应用情景下的一般化概念,相对地离系统的输入和输出更远。所以,用例依赖业务实体,而业务实体却并不依赖于用例。 还是以前面的“开播”和“频道”来理解,“开播”用例是直播app下的一个特定应用情景,需要关联着用户输入开播参数,开播后的输出推流状态等。而“频道”业务实体,是一个更一般化的概念,进出频道,频道状态变化等,都离具体的用户输入、效果展示输出更远。所以,“开播”会依赖到“频道”,而“频道”却不会依赖“开播”。 请求和响应模型 在通常情况下,用例会接收输入数据,并产生输出数据。但在一个设计良好的架构中,用例对象通常不应该知道数据展现给用户或其他组件的方式。很显然,我们当然不会希望这些用例类中的代码出现HTML和SQL。因此,用例类所接收的输入应该是一个简单的请求性数据结构,而返回输出的应该是一个简单的响应性数据结构。这些数据结构中不应该存在任何依赖关系,它们并不派生自HttpRequest和HttpResponse这样的标准框架接口。这些数据应该与web无关,也不应该了解任何有关用户界面的细节。这种独立性非常关键,如果这里的请求和响应模型不是完全独立的,那么用到这些模型的用例就会依赖于这些模型所带来的各种依赖关系。 用例的输入、输出模型要简单,就是一个类似java中的POJO对象,无其他任何依赖关系。 可能有些读者会选择直接中数据结构中使用对业务实体对象的引用。毕竟,业务实体与请求/响应模型之间有很多相同的数据。但请一定不要这样做!这两个对象存在的意义是非常、非常不一样的。随着时间的推移,这两个对象会以不同的原因,不同的速率发生变更。所以将它们以任何形式整合在一起都是对共同必包原则(CCP)和单一职责原则(SRP)的违反。这样做的后果,往往会导致代码中出现很多分支判断语句和中间数据。 martin在这本书里多个地方提到架构设计需要将“以不同原因,不同速率发生变更”的对象隔离开。CCP原则(见书第13章共同必包原则)和SRP(单一职责原则)可以用一句话来概括: 将由于相同原因而修改,并且需要同时修改的东西放在一起。将由于不同原因而修改,并且不同时修改的东西分开。 本章小结 业务逻辑是一个软件系统存在的意义,它们属于核心功能,是系统用来赚钱或省钱的那部分代码,是整个系统中的皇冠明珠。这些业务逻辑应该保持纯净,不要掺杂用户界面或者所使用的数据库相关的东西。在理想情况下,这部分代表业务的代码应该是整个系统的核心,其他低层概念的实现应该以插件形式接入系统中。业务逻辑应该是系统中最独立、复用性最高的代码。 回想经历过的项目中,最近负责的一个代码复用项目确实正如上述这段内容描述一般,保持纯业务逻辑,所依赖的外部实现均以接口注入方法接入。所以这个模块也得以在多个项目中复用。就是一个典型的例子。面对一个项目,最重要的是先找出其核心业务逻辑、核心功能来。识别出其中的业务实体 和 用例,把业务逻辑层设计好,整体系统设计就成功了一半了。","tags":[{"name":"architecture","slug":"architecture","permalink":"http://kmfish.github.io/tags/architecture/"}]},{"title":"LifyCycle 的思考","date":"2017-08-28T16:46:56.000Z","path":"2017/08/29/livedata-lifecycle/","text":"LifyCycle 的思考 大纲google 的LifeCycle、 livedataglide里的 lifeCycleListener 也是一样的实现分析下原理,对比下两者。自己的组件怎么使用这个东西 Android architecture components在包 android.arch.lifecycle 包下新增了一些组件,几个核心的包括 LifeCycle, LiveData, ViewModel我们先重点关注下 LifeCycle开头的这几个。Interfaces:LifecycleObserverLifecycleOwnerLifecycleRegistryOwner Classes: 处理生命周期LifecycleLifecycle is a class that holds the information about the lifecycle state of a component (like an activity or a fragment) and allows other objects to observe this state.Think of the states as nodes of a graph and events as the edges between these nodes.A class can monitor the component’s lifecycle status by adding annotations to its methods. 12345678910public class MyObserver implements LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) public void onResume() { } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) public void onPause() { }}aLifecycleOwner.getLifecycle().addObserver(new MyObserver()); LifeCycle 其实就是一个状态机内部有 state、event,然后接收新的Action,切换到newState。 LifecycleRegistry implement LifeCycle该类维护了一个Observer的列表,维护了当前的state,提供了添加、移除Observer、状态变更的接口。 LifecycleActivity、LifecycleFragment就使用该类来实现LifecycleOwner,负责上述这些职责。如果你有一个自己的LifecycleOwner类,那么可以直接组合使用该类。 这个类的核心逻辑,就是响应handleLifecycleEvent(event) 123456789101112131415161718192021222324 public void handleLifecycleEvent(Lifecycle.Event event) { if (mLastEvent == event) { return; } mLastEvent = event; mState = getStateAfter(event); // 根据新event切换state到新状态 for (Map.Entry<LifecycleObserver, ObserverWithState> entry : mObserverSet) { // 通知观察者同步state entry.getValue().sync(); } } // ObserverWithState class void sync() { if (mState == DESTROYED && mObserverCurrentState == INITIALIZED) { mObserverCurrentState = DESTROYED; } while (mObserverCurrentState != mState) { // 若观察者的状态和当前状态不一致,则逐个状态向上或向下切换并通知callback, 直至相同为止。 Event event = mObserverCurrentState.isAtLeast(mState) ? downEvent(mObserverCurrentState) : upEvent(mObserverCurrentState); mObserverCurrentState = getStateAfter(event); mCallback.onStateChanged(mLifecycleOwner, event); } } LifecycleOwner这是一个只有一个方法的接口,提供一个LifeCycle实例的方法 getLifycycle()。任何应用类可以实现该接口。一个通用的场景当这个LifeCycle当前未处在一个合适的状态时,避免执行某些回调。譬如,如果一个Fragment transaction回调在Activity state saved之后执行,就会发生crash,所以我们绝不希望此时调用这个回调。基于此,LifeCycle支持查询其当前状态。 12345678910111213141516171819202122232425class MyLocationListener implements LifecycleObserver { private boolean enabled = false; public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) { ... } @OnLifecycleEvent(Lifecycle.Event.ON_START) void start() { if (enabled) { // connect } } public void enable() { enabled = true; if (lifecycle.getState().isAtLeast(STARTED)) { // connect if not connected } } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) void stop() { // disconnect if connected }} 这个类就是生命周期感知的。它可以自己完成初始化和清理动作,而不需要Activity的管理。 能够和LifeCycle工作的类被称为是生命周期感知组件。那些和Android LifeCycle一起工作的类库被鼓励提供生命周期感知的组件,这样他们的客户可以轻松的集成这些类而不用手动管理生命周期。 LiveData就是一个生命周期感知组件的例子。 LifecycleDispatcher 绑定Activity的生命周期该类就是绑定并追踪application的Activity生命周期的关键类了,LifeCycle事件都是由它观察了Activity的事件后,再派发出来的。代码不多,实现原理也很简单, 初始化时,向Application注入一个ActivityLifecycleCallbacks来监听其生命周期变化并派发。 当Activity onCreate时,若activity instanceof FragmentActivity 为true,则向其supportfragmentManager注入一个FragmentLifecycleCallbacks接口,用来监听其内部的Fragment生命周期事件并派发,同时对其内部实现了LifecycleRegistryOwner的Fragment也添加一个DestructionReportFragment,用来接收onXXX事件以及派发事件。然后,继续向其getFragmentManager注入一个ReportFragment,利用它来接收Activity的onXXX事件以及派发通知。 LifecycleServicehttp://chaosleong.github.io/2017/05/27/How-Lifecycle-aware-Components-actually-works/ LiveDatahttp://chaosleong.github.io/2017/06/14/How-LiveData-actually-works/ Glide 中的LifeCycle因为最近在看LifeCycle的代码,偶然搜索类时发现很多类库自己也实现了LifeCycle的机制。譬如Glide里就也有LifeCycle接口,它也实现了自己的LifeCycle管理。以下简单分析下其源码: 其实Glide中的LifyCycle实现和android arch包的实现原理是一样的,也是向当前Activity中添加一个Fragment(RequestManagerFragment),依靠这个Fragment来接收系统组件的生命周期,然后再通知LifeCycle接口。RequestManager作为LifeCycleListener,最后会接收该LifeCycle的通知,从而控制自己管理的所有request的start、stop行为。图片加载的Target对象(如GlideDrawableImageViewTarget )也会接收通知,来控制动画的播放与停止。 使用LifeCycle来增强自己的类库学习了LifeCycle的思路,我们可以考虑给自己的类库也添加上这个特性,这样可以让类库更加“智能”,可以自动的跟随LifeCycleOwner 组件(也不一定只是系统组件,也可以是外部实现并传递进来的)的生命周期来控制自己的行为。如一个播放器类库,可以增加对生命周期的支持。当宿主Activity状态变更时,PlayerView可以自己暂停、恢复播放、销毁回收等,不再需要用户手动控制了。","tags":[]},{"title":"一个有趣的需求实现过程:人脸表情包制作","date":"2017-02-24T16:48:12.000Z","path":"2017/02/25/faceclipper/","text":"需求产品有个需求是使用用户上传的照片,自动生成目前比较火的表情包,如下图: 技术点 人脸识别,这是购买别人的SDK实现的啦 。 图像的处理,抠图、图像变换、裁剪、叠加到背景等。 实现过程简单分析了下,要实现这个效果,也有几个关键点要处理好: 图像灰度、亮度、对比度的调整 脸部区域扣取和边缘模糊 当然,如果要进一步优化下去,也有很多的细节了: 人脸方向和背景不一致,则需要调整 人脸背景色和表情背景色不一致的情况,要调整; 人脸图像大小和位置也需要调整。。。先从基础的这几个点介绍下吧。 图像效果处理这里我使用了GPUImage的滤镜,可以很方便的对原图进行处理。GPUImageGrayscaleFilter GPUImageContrastFilter GPUImageBrightnessFilter 组合使用这三个滤镜,完成灰度、对比度、亮度的调整即可。 脸部区域扣取和边缘模糊 计算脸部区域合适的Path根据识别SDK给出的人脸五官部位的特征点,来构造一个脸部五官区域的Path。这个Path的计算也经历了几个尝试,主要以两眉和嘴的坐标点来计算,最后的方案是 左眉最左边A,嘴唇下部B,右眉最右边C,从A出发,AB、BC、CA 顺序画了三条贝塞尔曲线,来达到有弧度的边缘效果。 扣取该Path区域一开始用了canvas.clipPath 来抠图,但发现边缘有锯齿,上网查资料得知,clipPath不能添加抗锯齿效果了,需要采用Paint 配置PorterDuffXfermode 的方式来实现。原理就是,先drawPath画一张Path区域大小的图A,然后再drawBitmap把A画到原图上去,采用PorterDuff.Mode.DST_IN 的模式即可仅保留住path的像素,去掉其余像素。关于PorterDuff模式的使用,也是有点小坑,可以参考下这篇blog。 边缘模糊效果如果clip的边缘没有模糊效果,那么融合到背景上去是很生硬的,所以需要添加模糊处理。在扣取这步drawPath的时候采用 1paint.setMaskFilter(new BlurMaskFilter(EDGE_BLUR, BlurMaskFilter.Blur.NORMAL)); 即可在path路径上增加模糊效果了。 裁剪合适的大小经过上述步骤后,已经得到了一个仅保留面部区域像素的bitmap(其他区域为透明),此时需要进行一次裁剪,得到一个仅面部大小的图片。在一番尝试后,最后基于path的bounds Rect,再向周围扩大一圈,来计算剪切的rect。这样可以保留住path周围的模糊边缘,才能达到更好的融合效果。 后续优化就这个功能来说,目前的效果也只是demo程度了。后续持续优化的话,可能就需要考虑人脸角度,原片亮度,背景","tags":[]},{"title":"通过flatmap组合多个SqlBrite的QueryObservable 使用的问题","date":"2016-12-21T11:16:18.000Z","path":"2016/12/21/SqlBrite-flatMap-multiple-queryObservable-cause-duplicates/","text":"阅读背景SqlBrite 是Square公司提供的一个数据库轻量级的封装框架。提供了RxJava的Observable风格的DB操作接口,其中一个特性是 其query操作得到的QueryObservable会一直保持对该次查询的表后续变更的事件的订阅,后续针对同一张(或多张)表的变更,均会再次发射数据给它的Subcriber,从而可以方便的实现界面的更新。 自定义的词汇含义: 订阅链、事件流: 这是指任意个Observable组合后,最终被某个Subscriber订阅时,所确定的一条订阅关系链。如: 12345 ObservableA.flatMap(ObservableB) .flatMap(ObservableC) .subscribe(new Subscriber<T>() {...} 问题背景我们最近的一个项目中使用了SqlBrite + SqlDelight框架 来实现我们的数据库层。同时也使用了RxJava。所以在项目代码中,有许多针对数据库操作的接口都是以Observable方式返回的,就自然的出现了一些以各种方式组合多个QueryObservable,再提供给外层的Subscriber订阅的情况。如下例子: 123456789101112131415161718192021queryA.flatMap(new Func1<String, Observable<String>>() { @Override public Observable<String> call(String s) { return getQueryB(s); }}).flatMap(new Func1<String, Observable<String>>() { @Override public Observable<String> call(String s) { return getQueryC(s); }}).subcribe(new Action1(String str) { // update UI});private QueryObservable getQueryB(String params) { return britedatabase.createQuery(....);}private QueryObservable getQueryC(String params) { return britedatabase.createQuery(....);} 上述代码段中的queryA、queryB、queryC均是使用SqlBrite的createQuery接口返回的QueryObservable,所以它们均具有一直订阅着自己关注表的变更的特性(假设它们分别查询了A、B、C三张表)。咋看之下,这个流程似乎也没什么问题,我们的代码一开始也就是这样写的了。但后来遇到些数据错乱的情况时,才定位到这个问题。设想这样的流程: 当完成对这个事件流的订阅后,A表发生了1次变化,则queryA会发射1个数据到流里,那么接下来的getQueryB、getQueryC 方法均会执行1次,又由于createQuery每次都会创建QueryOBservable的实例,所以getQueryB执行1次,就创建了1个QueryObservableB的实例了,getQueryC同理,然后subcriber处会接收到1次数据。 经过1之后,此时B表发生 了1次变化,那么getQueryC会执行一次,subscriber会收到1次事件,真的是这样吗?结论肯定是NO了。实际测试发现,此时subcriber会收到2次数据。 追根溯源有点摸不着头脑了吧,我们再回顾之前的流程,会发现getQueryB 方法在整个流程中执行了2遍,说明创建了两个QueryObservableB的实例。当B表变化时,看起来像是这两个queryB的实例接收到了SqlBrite发射的数据了。那究竟这两个queryB被谁订阅了呢?这就需要去看下RxJava的源码了,queryA和 getQueryB 是使用了flatMap操作符来连接的,所以我们看一下flatMap的实现: 12345678910111213141516171819202122232425262728293031323334public final <R> Observable<R> flatMap(Func1<? super T, ? extends Observable<? extends R>> func) { if (getClass() == ScalarSynchronousObservable.class) { return ((ScalarSynchronousObservable<T>)this).scalarFlatMap(func); } // 重点就在这一行了,我们可以发现flatMap其实就是先map,然后再merge return merge(map(func)); }public final <R> Observable<R> map(Func1<? super T, ? extends R> func) { return create(new OnSubscribeMap<T, R>(this, func)); // map的实现就是OnSubscribeMap,其实就是包装了一个内部订阅者来订阅上游的Observable,当收到上游的数据时,执行func来转换数据类型,再发射新数据给下游订阅者。}public final class OperatorMerge<T> implements Operator<T, Observable<? extends T>> { static final class MergeSubscriber<T> extends Subscriber<Observable<? extends T>> { .. . @Override public void onNext(Observable<? extends T> t) { if (t == null) { return; } if (t == Observable.empty()) { emitEmpty(); } else if (t instanceof ScalarSynchronousObservable) { tryEmit(((ScalarSynchronousObservable<? extends T>)t).get()); } else { InnerSubscriber<T> inner = new InnerSubscriber<T>(this, uniqueId++); // 可以看到这里有个InnerSubscriber addInner(inner); t.unsafeSubscribe(inner); emit(); } } } merge 操作符由于其代码比较多且有些复杂,我也只是简单分析了下,其内部使用了一个MergeSubscriber来代理上游的数据,然后让下游订阅它。MergeSubscriber内部使用了InnerSubscriber的集合来订阅从上游接收到的每个Observable,然后再把接收的具体的data依次发射到下游,可以参考merge的弹珠图flatMap的弹珠图 经过对flatMap的分析,我们可以知道queryA发射数据,然后map操作符内部订阅了queryA,经过func转换后发射了多个QueryObservableB给merge操作符(因为这个func1是从String –> Observable),merge操作符会在内部分别订阅收到的多个QueryOBservableB。分析到这里,就明白了我们的场景下,两个QueryOBservableB的实例都是被merge操作符给订阅了,所以当B表变化,SqlBrite会发射数据给到这两个queryB,从而最终传递到subscriber处,就得到了2次数据。 解决方案探讨针对这个问题,我们尝试了几种解决思路: 避免这样的多个QueryObservable通过flatMap连接的情况,直接通过sql语句来做联合查询。这样是回避了问题场景,但也确实有一些场景下还是会需要组合多个QueryObservable的,如先queryA,然后再到Server上查询一个结果b,最后再拿b去作为queryC的查询参数,这种情况下,queryA、queryC还是需要连接在一个事件流中了。 当多个QueryObservable连接时,从第二个开始的query,均不再监听表的变化,如添加一个 .first()。但这样的话,若B、C表将来变化时,最终的subcriber是不会更新的。又考虑,可以在queryA里去监听整个事件流中涉及到的所有DB table,无论任何一张表变化,整个事件流均重新query一遍。但再一细想,这个做法也是不行的,queryA根本不可能掌握到整个事件流里到底需要监听哪些表,因为queryB、queryC并没有暴露这个信息,除非去看queryB、queryC的实现,但那样又都全部耦合了。而且queryA、queryB、queryC这样的方式,还有一个好处就是一个QueryObservable可以被复用,根据需要,既可以独立使用,也可以结合其他的query组合使用,而2方案的话,也会破坏这个特性了。 我们采用的方案实现一个QueryObservableManager对象,来统一管理每次事件流中的QueryObservable的订阅,其内部维护一个当前事件流中已有的query订阅关系集合。对原始的createQuery生成的QueryObservable进行一层装饰,由调用者传递一个context参数,用来唯一标识出一个事件流中的一个QueryObservable。当DecorationObservable被订阅时,根据context查询,把之前存在的订阅关系进行退订。DecorationObservable内部再订阅raw query Observable。这让最终流的订阅者增加了两个因素的变化,一个是QueryObservableManager,一个是context参数。 如何标识一个QueryObservable的订阅通过分析,如果针对单个订阅链使用一个QueryObservableManager,那么在一个事件链中的不同QueryObservable可以利用return 该Observable的Method栈信息(StackTraceElement)来做context,来唯一标识。如上文例子的 getQueryB 这个方法。因为之前提到,getQueryB是可以复用的,在不同的订阅链中都可能出现,所以在不同订阅链的情况下,就不能仅仅以方法调用栈来标识了(此时两个实例的method栈信息是一样的,但其实是需要同时保持两个订阅的)。但对于一个订阅链来说,出现两个相同的栈时,意味着有多个相同的query被订阅了,此时就需要把之前的query给退订,保留最后一次订阅。一句话,一个订阅链中,同一个getQuery 方法的Observable订阅只能保持一个。 所以,这个context就不用外部传了,直接在QueryObservableManager 内部来确定即可。 如何在一个订阅链中,使用一个QueryObservableManager ?通过将所有的类似 getQueryB的方法,均增加一个参数QueryObservableManager,在最终Observable被订阅时,由订阅者构造一个QueryObservableManager 实例,从而保证了在一个订阅链中,使用的都是同一个QueryObservableManager 对象。 Demo测试项目QueryObservableManage实现 Demo 项目 相关链接SqlBrite项目主页上也有人遇到这个问题了","tags":[{"name":"SqlBrite","slug":"SqlBrite","permalink":"http://kmfish.github.io/tags/SqlBrite/"},{"name":"RxJava","slug":"RxJava","permalink":"http://kmfish.github.io/tags/RxJava/"}]},{"title":"修复一例 BlockingQueue.poll 导致的线程While循环无限执行占用cpu的bug","date":"2016-05-26T08:19:27.000Z","path":"2016/05/26/fix-while-loop-cpu-use-problem-md/","text":"起因基于部分用户反馈使用我们的app时,玩游戏过程中会有卡顿现象出现,从而进行cpu使用率排查。 发现问题今天先操作进入一个房间后,使用android的traceview跟踪了一段2s左右的cpu使用数据,然后通过traceview对其进行分析。 如图一所示,左边列出了该时间段内的线程,右边则图形化的显示了它们的cpu使用情况。很直观的能够发现,”GroupMsgTransport”这个线程几乎一直在占用cpu,比main线程还多的多。所以首先引起怀疑。 再进一步查看详细的cpu使用情况,发现占用cpu time最多的几项都是在对一个BlockingQueue的操作,选择这些行之后,也定位到了 “GroupMsgTransport”线程。说明这条线程一直在对Queue执行poll操作。通过在代码中搜索关键字,查找到了这个罪魁祸首。 定位原因最终定位到这段问题代码: 123456789101112131415@Override public void run() { android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); while (run) { final ImGroupMsgInfo info; info = mMQ.poll(); // 罪魁祸首的一行, poll()方法在队列为空时会return null,从而就导致该循环无限执行下去,空耗了cpu if (info != null) { //... } else { // do nothing } } } 修复知道了根本原因后,修复的方法也很简单,将poll换为take() 即可,take方法在队列为空时会block住当前thread,从而不再占用cpu。 1234567891011121314151617181920@Override public void run() { android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); while (run) { ImGroupMsgInfo msgInfo = null; try { msgInfo = mMQ.take(); // 修改此处为take() } catch (InterruptedException e) { MLog.error(TAG, e); } final ImGroupMsgInfo info = msgInfo; if (info != null) { //... } else { // do nothing } } } 结果对比修改之前,cpu占用高达近50%: 修改之后,cpu占用不到10%: 后记在修复了这一处问题后,又继续在项目里搜索了下使用poll()的地方,结果就又发现了一处一模一样的bug。。。以后可以多留意while(true)循环,要注意循环是否有正确的退出时机,或block的时机,避免出现类似问题。","tags":[{"name":"性能优化","slug":"性能优化","permalink":"http://kmfish.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"}]},{"title":"切换到Hexo + GithubPages","date":"2016-01-17T07:02:55.000Z","path":"2016/01/17/change-to-hexo/","text":"博客之前用的是sina的SAE + press,也一直还不错。其实最早一开始用的是AWS的EC2,但连接速度实在太慢,才换到SAE的。然后参加活动获得了一些云豆,本以为够用蛮久的,结果最近一两个月,突然的访问把云豆都用光了。于是,就打算切换到githubpages上,昨晚在网上看了下hexo来搭建博客,看上去蛮不错的,于是就切换过来了。这次应该就不怕流量用完了,而且hexo的使用也蛮方便的,网上的各种资料也挺多,感谢这些作者,给大家带来了便利。","tags":[]},{"title":"MultiTypeListViewAdapter Android ListView 多type的Adapter封装","date":"2016-01-15T16:00:00.000Z","path":"2016/01/16/MultiTypeListViewAdapter/","text":"MultiTypeListViewAdapter 是什么?MultiTypeListViewAdapter,顾名思义。其封装了多type下的Adapter的编程模式,通过对每种type统一接口,利用多态的方式,将type的实现从Adapter中抽离出来。Adapter只需面向统一接口,所以可以提供一个通用实现,实现代码不再变动。而会变动的每个type对应的item实现,则由使用者自己去实现。对扩展开放,对修改封闭。 同时,由于每个type的item均被抽离出来了。相当于复用的粒度为每个type item,可以根据需要,动态地选择合适的item去添加到adapter中。提高了代码复用,每个人编写维护好自己的item即可,避免了多人合作时都去修改Adapter,容易造成冲突。 另外,由于ViewHolder 模式的规范,MultiTypeListViewAdapter也同时封装了ViewHolder模式。常见的ListView Adapter 实现先看一下常见的ListAdapter 实现,分为单个type和多type两种情况。 1、单Type的Adapter12345678910111213141516171819202122232425262728293031323334353637383940414243class ListAdapter extend BaseAdapter { ... private List<String> contents = new ArrayList(); ... public View getView(int position, View convertView, ViewGroup parent) { String item = getItem(position); if(null == item) { throw new RuntimeException("list item is never null. pos:" + position); } else { ViewHolder holder; if(null == convertView) { holder = createViewHolder(parent); convertView = holder.itemView; convertView.setTag(holder); } else { holder = (ViewHolder)convertView.getTag(); } item.updateHolder(holder, position); return convertView; } } } private ViewHolder createViewHolder(ViewGroup parent) { // create item view from layout xml ... } private void updateHolder(ViewHolder holder, String item) { // update item view holder.text.setText(item); } private class ViewHolder { TextView text; ViewHolder(View itemView) { text = (TextView)itemView.findViewById(R.id.text); } } ...} 应该是挺常见的写法吧,继续往下。 2、多type的adapter1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586class ListAdapter extend BaseAdapter { ... private List<String> contents = new ArrayList(); ... public int getItemViewType(int position) { if (position % 2 == 0) { return 0; } return 1; } public int getViewTypeCount() { return 2; } public View getView(int position, View convertView, ViewGroup parent) { String item = getItem(position); if(null == item) { throw new RuntimeException("list item is never null. pos:" + position); } else { ViewHolder holder; if(null == convertView) { if (getItemViewType(position) == 0) { holder = createViewHolder(parent); } else if (getItemViewType(position) == 1) { holder = createViewHolder1(parent); } convertView = holder.itemView; convertView.setTag(holder); } else { holder = (ViewHolder)convertView.getTag(); } if (getItemViewType(position) == 0) { item.updateHolder(holder, position); } else if (getItemViewType(position) == 1) { item.updateHolder1(holder, position); } return convertView; } } } private ViewHolder createViewHolder(ViewGroup parent) { // create item view from layout xml ... } private void createViewHolder1(ViewGroup parent) { // create item view from layout xml ... } private void updateHolder(ViewHolder holder, String item) { // update item view holder.text.setText(item); } private void updateHolder1(ViewHolder1 holder, String item) { // update item view holder.text.setText(item); holder.img.setImageResoure(R.drawable.img); } private class ViewHolder { TextView text; ViewHolder(View itemView) { text = (TextView)itemView.findViewById(R.id.text); } } private class ViewHolder1 { TextView text; ImageView img; ViewHolder(View itemView) { text = (TextView)itemView.findViewById(R.id.text); img = (ImageView)itemView.findViewById(R.id.img); } } ...} 看到这里,是否感觉这个Adapter的getView()开始有些让人不舒服了呢。若type再进一步增加,难不成还得继续if/else下去,adapter变得又臭又长。估计往后下去,就再没人愿意维护了。而且,这个Adapter的这一堆代码还得到处重复写下去,每个有listView的地方,都得配套一个adapter。针对这个坑,我设计了MultiTypeListViewAdapter来解决。希望能帮助到有需要的程序猿们~ 使用 MultiTypeListViewAdapter123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110public class MainActivity extends AppCompatActivity { private ListView listView; private MultiTypeArrayAdapter adapter; private static final int ITEM_TYPE_1 = 0; private static final int ITEM_TYPE_2 = 1; private static final int ITEM_TYPE_COUNT = 2; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listView = (ListView) findViewById(R.id.listview); adapter = new MultiTypeArrayAdapter(ITEM_TYPE_COUNT); listView.setAdapter(adapter); setupAdapter(); } private void setupAdapter() { adapter.setTypeCount(ITEM_TYPE_COUNT); LineListItem1 item1 = new LineListItem1(this, ITEM_TYPE_1, "line type 1"); LineListItem2 item2 = new LineListItem2(this, ITEM_TYPE_2, "line type 2"); adapter.setNotifyOnChange(false); for (int i = 0, len = 50; i < len; i++) { adapter.addItem( i % 2 == 0 ? item1 : item2); } adapter.notifyDataSetChanged(); }}public class LineListItem1 extends BaseListItem { private String line; public LineListItem1(Context mContext, int viewType, String line) { super(mContext, viewType); this.line = line; } @Override protected int onGetItemLayoutRes() { return R.layout.list_item1; } @Override protected ViewHolder onCreateViewHolder(View itemView) { return new LineViewHolder(itemView); } @Override public void updateHolder(ViewHolder holder, int pos) { LineViewHolder h = (LineViewHolder) holder; h.text.setText(line + "_" + pos); } private class LineViewHolder extends ViewHolder { TextView text; public LineViewHolder(View itemView) { super(itemView); text = (TextView) itemView.findViewById(R.id.line_text); } }}public class LineListItem2 extends BaseListItem { private String line; public LineListItem2(Context mContext, int viewType, String line) { super(mContext, viewType); this.line = line; } @Override protected int onGetItemLayoutRes() { return R.layout.list_item2; } @Override protected ViewHolder onCreateViewHolder(View itemView) { return new LineViewHolder2(itemView); } @Override public void updateHolder(ViewHolder holder, int pos) { LineViewHolder2 h = (LineViewHolder2) holder; h.text.setText(line + "_" + pos); h.img.setImageResource(R.drawable.icon_git); } private class LineViewHolder2 extends ViewHolder { ImageView img; TextView text; public LineViewHolder2(View itemView) { super(itemView); img = (ImageView) itemView.findViewById(R.id.thumb); text = (TextView) itemView.findViewById(R.id.name); } }} 可以看到,使用MultiTypeListViewAdapter之后,实现多type的Adapter变得相当简单了。再也不用面对一堆判断itemType的if/else了。每增加一种type,只需增加一种新的ListItem即可,再也不用去改动Adapter的代码了。 项目发布在github上了,MultiTypeListViewAdapter,欢迎fork和交流。","tags":[{"name":"Android","slug":"Android","permalink":"http://kmfish.github.io/tags/Android/"},{"name":"ListView","slug":"ListView","permalink":"http://kmfish.github.io/tags/ListView/"}]},{"title":"android 子线程中更新界面?被ProgressBar给迷惑了","date":"2015-09-22T06:51:37.000Z","path":"2015/09/22/Can-update-view-in-background-thread/","text":"在看apidemos的例子RetainedFragement时,看到在Thread中执行了 这么一句 1mProgressBar.setProgress(progress); 且执行正常,progressbar确实一直在更新。顿觉疑惑,View在更新时,会检查当前线程是否是创建view所在的线程(即UI线程),若不一致,则会抛出异常的。 in ViewRootImpl.java 中: 123456void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); }} 后来查看了setProgress()的源码后,才恍然大悟,这个方法内已经处理了子线程里调用的情况了。 1234567891011121314151617181920212223242526272829303132333435363738394041@android.view.RemotableViewMethod public synchronized void setProgress(int progress) { setProgress(progress, false); } @android.view.RemotableViewMethod synchronized void setProgress(int progress, boolean fromUser) { if (mIndeterminate) { return; } if (progress < 0) { progress = 0; } if (progress > mMax) { progress = mMax; } if (progress != mProgress) { mProgress = progress; refreshProgress(R.id.progress, mProgress, fromUser); } } private synchronized void refreshProgress(int id, int progress, boolean fromUser) { if (mUiThreadId == Thread.currentThread().getId()) { doRefreshProgress(id, progress, fromUser, true); } else { if (mRefreshProgressRunnable == null) { mRefreshProgressRunnable = new RefreshProgressRunnable(); } final RefreshData rd = RefreshData.obtain(id, progress, fromUser); mRefreshData.add(rd); if (mAttached && !mRefreshIsPosted) { post(mRefreshProgressRunnable); mRefreshIsPosted = true; } } } 关键就是refreshProgress()了,这里处理了若当前线程不是ui thread,则将更新消息post到ui thread中去执行了。而且可以发现这几个方法都是加了同步控制的,是线程安全的,保证了多线程调用也是正常的了。 真的是“源码之下无秘密“啊。 p.s:之前一次面试的时候,对方有问到能否在子线程里更新界面?我回到不能,因为会抛出异常。对方接着问,为什么?我就答不上来了,现在的理解是,因为android下的界面更新不是线程安全的,所以要保证在单一线程中同步执行。其他线程要更新UI的话,需要通过handler,Looper消息机制把更新事情pass到UI thread的消息队列中,由UI thread来完成界面的更新绘制。面试官又继续问道,能否在子线程里去更新progressbar的进度呢?我当时回答的是不能,必须通过handler去发消息。但现在看来progressbar的代码后,这个问题的答案其实应该是可以的,因为progressbar自己内部就处理了子线程的更新问题。 学东西,要能做到知其然,知其所以然。我对知识的掌握层次还太浅了,仅仅停留在会使用工具的层面。平时要多思考,自己给自己提出一些问题(what,why),才能加深对知识的理解。","tags":[]},{"title":"如何获取 Gopro 视频流","date":"2015-08-31T07:28:34.000Z","path":"2015/08/31/Get-gopro-video-stream/","text":"前言:最近的工作在研究gopro的视频流如何获取,通过搜索资料,以及对gopro app的抓包分析,得出了以下经验。这次的分析过程也体会到抓包分析的好处,以后还应进一步学习如何用好抓包工具。 如何获取Gopro的视频流以下的这些url会随gopro的型号版本而有所不同,需要自己抓包分析确定。 gopro提供了wifi和hdmi的视频输出,目前研究的是wifi输出。通过对gopro app抓包分析,和网上搜索的资料,整理出以下方法: 先通过gopro app完成和gopro的蓝牙配对,配置好wifi热点,并开启wifi热点; 手机切换至gopro的热点网络,在 udp://:8554 上开启监听。 发送http get request( http://10.5.5.9/gp/gpExec?p1=gpStreamA9&c1=restart),gopro即开始向手机的 8554 端口发送数据。手机即可在udp 8554端口收到数据。 通过抓包分析,gopro app会定期(1次/s)发送心跳请求,维持连接。udp : “GPHD:1:0:2:0” 0.3次/s // 若无此心跳,则视频流几秒后会中断http : GET /gp/gpControl/status 2次/s //若无此心跳,则gopro的wifi热点几分钟后会关闭 备注:抓包分析出gopro通信的一些url,响应均为jsonGET /bacpac/cv 返回gopro热点名称GET /gp/gpControl 返回gopro所有设置项的当前状态,可选项信息,以及所有支持的url command如: /gp/gpControl/command/wireless/pair/cancel 如果已经配对成功则取消配对 其他包gopro app 会发送mdns请求,获取gopro的ip地址及mac地址等信息;gopro app 每次进入preview界面时,会向全网发送WOL包,WOL(Wake on Lan)局域网唤醒,远程唤醒设备。 视频流格式,码率h.264,aac,mpegts流录制视频:码率范围:20mbps~60mbpshttp://zh.gopro.com/support/articles/hero4-black-recording-time-in-each-video-setting 实时预览下码率为:1mbps,帧率为30fps 上传视频1、wifi连接gopro,可以通过移动网络传输视频流ios下,可以在连接wifi热点的同时,其他数据通信自动走移动网络通道完成。android,5.0提供了新的网络api,可以给app指定使用特定网络。","tags":[]},{"title":"组合还是继承","date":"2015-08-08T13:59:37.000Z","path":"2015/08/08/组合还是继承/","text":"如题,每天在“面向对象”的我们,也常常遇到这个选择吧。刚好看书书读到这个话题,就记录下阅读的理解。 继承要慎用,其使用场合仅限于你确信使用该技术有效的情况。一个判断方法是,问一问自己是否需要从新类向基类进行向上转型。如果是必须的,则继承是必要的。反之则应该好好考虑是否需要继承。——《java编程思想 第四版》 p140 复合优于继承 与方法调用不同的是,继承打破了封装性。换句话说,子类依赖于其超类中特定功能的实现细节。 只有当子类真正是超类的子类型时,才适合用继承。换句话说,对于两个类A和B,只有当两者之间确实存在“is-a”关系的时候,类B才应该继续类A。 为了避免这种脆弱性,可以用复合和转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候。包装类不仅比子类更健壮,而且功能也更加强大。 ——《Effective Java》 item.16 在平时工作过程中,我自己也是更多选择组合而不是继承。但也没有个明确清晰的判断标准。和同事交流时,虽然也推荐多用组合,但也说不出个具体的缘由。平时主要是开发android app,在UI部分的代码里,自己也是大量使用组合,譬如view的复用,一些逻辑部分也是可以通过建一个xxxController,xxxManager来复用。减少Activity,Fragment内的代码量。 之前遇到过项目里的一个情形,几个类似布局的页面,实现是共同继承了一个基类。但这个基类并没有任何地方直接引用到,即项目内没有子类向上转型的地方。我认为仅仅是为了代码复用,是完全不需要继承的,把代码提到一个新类中,组合使用即可。后来这些页面之间由于需求问题,开始有更多的地方不同了,如果是在继承的情况下,改动起来反而更加复杂。因为不能轻易的去修改基类,可能会影响到其他页面。组合可以更灵活,我可以有选择的复用共同的部分,而自己继续写不同的地方,不会一股脑的继承了所有基类的细节,不用去考虑基类里的具体细节。","tags":[]},{"title":"Why use fragment?","date":"2014-09-21T13:49:13.000Z","path":"2014/09/21/Why use fragment/","text":"针对这个问题,在网上搜索了下,有以下一些看法: 1、fragments are more reusable than custom views. 作者认为,fragment能够封装单靠view无法处理的事。自定义控件无法处理跨activity交互等事件,还是需要靠activity来完成,这会增加custom views 和activity的耦合。不利于复用。而fragment可以封装更多的东西,包括AsyncTask,file and database access等。 2、the main reason to use Fragments are for the backstack and lifecyclefeatures. Otherwise, custom views are more light weight, simpler toimplement, and have the advantage that they can be nested.see here 主要原因是fragment提供了backstack支持界面状态的回退,以及生命周期特性。(在SDK 17 以后fragment也支持嵌套了) 过去的模式:如果实现官方文档中的那个例子,手机版上是两个activity(列表和详情),pad版上则在同一activity下了。在fragment推出之前,我们之前的项目里也有实现过类似的需求。两个界面分别为 listView 和 detailView,则activityA里会同时持有两个view,当判断为手机版,则detailview会hide;当为pad版时,才会显示detailView。然后activityA里就会同时包含对两个的view的控制代码,当然也可以提出一个viewController来包含控制代码,activityA来引用viewController。这里的这个ViewController(或viewManager)就类似fragment。按照MVC的思想,activity其实就扮演了Controller的角色(或者再自己封装一个viewManager,但与系统的交互还是由activity来负责管理的。) fragment的优点:fragment同样有自己的生命周期,不用刻意担心内存泄漏的问题,而且fragment还拥有切换保留(retain)机制,可以在状态切换期间保持状态不变(如后台任务等)。另外,由于框架已经实现了对fragment的生命周期管理,所以开发时也不必在activity的生命周期回调事件代码中加入对各个组件的管理代码,代码整体更加简洁清晰。使用fragment,可以帮助开发者省下许多开发和维护的工作量。 自己的理解:通过查看fragment提供的接口,可以发现非常类似activity的接口,可以认为fragment is a “small activity“,所以相对于使用自定义view+ 控制器的方式,使用fragment可以封装更多的和系统的交互,提供更高级的封装。自定义view仅是在视图层面的封装(能够控制view的show,hide,布局的变化等),但fragment可以提供更高层面的控制(与activity交互,状态栈回退,独立的生命周期管理)。旧的方式依然需要依赖和activity的配合才能实现这些需求(如和其他activity的交互 startActiviyForResult,就可以直接在fragment里去调用了)。从整体上看,相当于将activity和可复用组件进一步解耦了,使得组件的复用性和封装性更好。activity不用再去向内部组件通知各项事件。而组件自身封装得更加完备,内部就处理了生命周期事件以及其他与系统的交互事件。在不同activity之间的复用变得更加容易灵活。从代码层面上看,activity里的代码就会少了许多,而各fragment的代码都由自己维护了。代码结构更加清晰简洁,大大降低了维护代价,也提高了项目代码的可读性。","tags":[]},{"title":"android camera 摄像头预览画面变形","date":"2013-09-08T14:59:37.000Z","path":"/?p=14","text":"问题:最近在处理一下camera的问题,发现在竖屏时预览图像会变形,而横屏时正常。但有的手机则是横竖屏都会变形。 结果:解决了预览变形的问题,同时支持前后摄像头,预览无变形,拍照生成的jpg照片方向正确。 环境 : android studio, xiaomi m1s android4.2 miui v5 过程: 1. 预览 preview画面变形以sdk中apidemos里的camera为例,进行修改。先重现下问题,在AndroidManifest.xml中指定了activity 的screenOrientation为landspace,则预览正常。若指定为portrait,则图像会有拉伸变形。 找到正确的previewsize继续看demo代码,mCamera.getParameters().getSupportedPreviewSizes() 可以返回当前设备支持的一组previewSize,例如: 1920x1088 1280x720 800x480 768x432 720x480 640x480 576x432 480x320384x288 352x288 320x240 240x160 176x144 而我们根据我们在界面上需要显示的预览大小,来设置camera的预览大小,即在这一组size中选择一个previewsize,找到高宽比和大小最接近的一个size。通过调用 getOptimalPreviewSize(List sizes, int w, int h) 来进行选择。注意这里的后两个参数,因为摄像头的预览size是固定的,就那么一组,其高宽比是固定的,且方向也是固定的。对于摄像头来说,都是width是长边,即width > height。 所以camera的ratio计算值总是大于1的。所以当手机在横屏的时候,我们的w>h,调用该方法进行选择是没问题的。但是当竖屏后,w < h了,若还是直接调用该方法,则targetRatio 会小于1,按这个targetRatio去找就找不到合适的size了,那么比例不对预览自然就变形了。所以得在调用的地方进行调整,保证参数w > h。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647private Size getOptimalPreviewSize(List<Size> sizes, int w, int h) { final double ASPECT_TOLERANCE = 0.1; double targetRatio = (double) w / h; if (sizes == null) return null; Size optimalSize = null; double minDiff = Double.MAX_VALUE; int targetHeight = h; // Try to find an size match aspect ratio and size for (Size size : sizes) { double ratio = (double) size.width / size.height; if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue; if (Math.abs(size.height - targetHeight) < minDiff) { optimalSize = size; minDiff = Math.abs(size.height - targetHeight); } } // Cannot find the one match the aspect ratio, ignore the requirement if (optimalSize == null) { minDiff = Double.MAX_VALUE; for (Size size : sizes) { if (Math.abs(size.height - targetHeight) < minDiff) { optimalSize = size; minDiff = Math.abs(size.height - targetHeight); } } } return optimalSize;} @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // We purposely disregard child measurements because act as a // wrapper to a SurfaceView that centers the camera preview instead // of stretching it. final int width = resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec); final int height = resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec); setMeasuredDimension(width, height); if (mSupportedPreviewSizes != null) { mPreviewSize = getOptimalPreviewSize(mSupportedPreviewSizes, Math.max(width, height), Math.min(width, height)); } ...... 选择和previewsize一致的比例来布局surfaceview当我们选择了合适的previewsize后,还有一个因素会影响到预览画面是否正常。即surfaceview的布局。从demo代码可以知道要想在界面上显示camera预览画面,需要添加一个surfaceview,而我们添加了surfaceview后,就需要对其进行布局,设置其大小,位置。 经过搜索查资料,here这里有人回答了原因。 “The reason is: SurfaceView aspect ratio (width/height) MUST be same as Camera.Size aspect ratio used in preview parameters. And if aspectratio is not the same, you’ve got stretched image.” 看到这句话,理解了变形是因为比例错误。surfaceview和cameraSize的比例应该要一致。 在onlayout中,我们对surfaceview进行了layout,根据指定的surfaceview的高宽来布局。demo中会将surfacevidew居中,看代码是根据宽高比和previewsize的宽高比来对比,选择水平居中或垂直居中。前面已经说过,previewsize的width大于height,所以凡是涉及到宽高比计算的地方,两个size我们都需要保持同样的顺序,都是w > h,或w < h。所以当竖屏时,我们在判断前应该交换presize的w和h,才能正确布局居中surfaceview。 1234567891011121314151617181920212223242526272829303132333435 protected void onLayout(boolean changed, int l, int t, int r, int b) { int curOrientation = getContext().getResources().getConfiguration().orientation; if (changed && getChildCount() > 0 || mLastOrientation != curOrientation) { mLastOrientation = curOrientation; final View child = getChildAt(0); final int width = r - l; final int height = b - t; int previewWidth = width; int previewHeight = height; if (mPreviewSize != null) { previewWidth = mPreviewSize.width; previewHeight = mPreviewSize.height; if (curOrientation == Configuration.ORIENTATION_PORTRAIT) { previewWidth = mPreviewSize.height; previewHeight = mPreviewSize.width; } } // Center the child SurfaceView within the parent. if (width * previewHeight > height * previewWidth) { final int scaledChildWidth = previewWidth * height / previewHeight; child.layout((width - scaledChildWidth) / 2, 0, (width + scaledChildWidth) / 2, height); } else { final int scaledChildHeight = previewHeight * width / previewWidth; child.layout(0, (height - scaledChildHeight) / 2, width, (height + scaledChildHeight) / 2); } }} 至此,经过测试,设备显示正常,横屏竖屏均再无拉伸现象了。 总结下,demo工程在横屏下正常,而在竖屏下出现预览画面变形的原因主要是,onMeasure时选择previewsize和onlayout时布局surfaceview,都是基于横屏考虑的,所以w均大于h。当activity改为竖屏运行时,就需要调整这两个地方,保证比例一致,才能计算正确,从而显示正常画面。 2. 方向问题刚刚讲了变形的问题,我们应该发现当手机竖屏后,预览画面的方向没有随之改变过来,于是看上去就颠倒了。所以我们还需要处理一下方向的问题。 注意这里的“方向”包括:前置、后置摄像头画面预览方向,前置后置摄像头拍照后的图片方向。 预览方向:通过查询android文档,可以发现如下资料: For example, suppose the natural orientation of the device isportrait. The device is rotated 270 degrees clockwise, so the deviceorientation is 270. Suppose a back-facing camera sensor is mounted inlandscape and the top side of the camera sensor is aligned with theright edge of the display in natural orientation. So the cameraorientation is 90. The rotation should be set to 0 (270 + 90). (后置摄像头)可以得知,camera是在设置上是固定方向的, camera的顶部是和屏幕自然显示时的右边对齐的。说明camera默认就是横屏方向的。为了在竖屏的时候进行preview预览,我们需要调整camera的方向。 setDisplayOrientation 可以修改camera的预览方向。 If you want to make the camera image show in the same orientation asthe display, you can use the following code. 123456789101112131415161718192021222324public static void setCameraDisplayOrientation(Activity activity, int cameraId, android.hardware.Camera camera) {android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();android.hardware.Camera.getCameraInfo(cameraId, info);int rotation = activity.getWindowManager().getDefaultDisplay() .getRotation();int degrees = 0;switch (rotation) { case Surface.ROTATION_0: degrees = 0; break; case Surface.ROTATION_90: degrees = 90; break; case Surface.ROTATION_180: degrees = 180; break; case Surface.ROTATION_270: degrees = 270; break;}int result;if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { result = (info.orientation + degrees) % 360; result = (360 - result) % 360; // compensate the mirror} else { // back-facing result = (info.orientation - degrees + 360) % 360;}camera.setDisplayOrientation(result);} 拍照方向:若想修改照片的方向,还需调用 camera.setRotation,google 的文档说的很清楚了,添加一个orientation listener既可以从传感器获取当前设备旋转方向,参考如下代码即可正确设置照片方向。但要注意一点,这个方向和activity方向(可以在android-manifest设置)无关。 无论activity此时是什么方向,只要获取了传感器方向均可以正确调整照片方向,与预览方面一致。 代码方面,由于这个回调调用比较频繁(设备角度一变化就会调用),可以在回调里保存下rotation,然后在拍照的时候再设置camera。由于考虑了前置摄像头,须注意传递正确的cameraId。 12mCamera.setParameters(parameters);mCamera.takePicture(shutterCallback, rawCallback,jpegCallback); CameraInfo.orientation is the angle between camera orientation andnatural device orientation. The sum of the two is the rotation anglefor back-facing camera. The difference of the two is the rotationangle for front-facing camera. Note that the JPEG pictures offront-facing cameras are not mirrored as in preview display. Forexample, suppose the natural orientation of the device is portrait.The device is rotated 270 degrees clockwise, so the device orientationis 270. Suppose a back-facing camera sensor is mounted in landscapeand the top side of the camera sensor is aligned with the right edgeof the display in natural orientation. So the camera orientation is90. The rotation should be set to 0 (270 + 90). The reference code is as follows. 1234567891011121314public void onOrientationChanged(int orientation) { if (orientation == ORIENTATION_UNKNOWN) return; android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo(); android.hardware.Camera.getCameraInfo(cameraId, info); orientation = (orientation + 45) / 90 * 90; int rotation = 0; if (info.facing == CameraInfo.CAMERA_FACING_FRONT) { rotation = (info.orientation - orientation + 360) % 360; } else { // back-facing camera rotation = (info.orientation + orientation) % 360; } mParameters.setRotation(rotation); } 3. 总结到此为止,我们基本上是实现了一个最简单的拍照应用,能支持前后摄像头,预览正确,照片方向正确。文中一些api在低版本sdk上没有,不能直接用,还须参考资料换用其他方法。由于手头设备有限,我仅仅在android4.2 小米手机上测试过,pad未测试。 补充setRotation 在一些设备上无效 拍照后得到的图像还是横屏的,所以在save前需要再rotate一下。 123456789101112131415161718private Bitmap adjustPhotoRotationToPortrait(byte[] data) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeByteArray(data, 0, data.length, options); if (options.outHeight < options.outWidth) { int w = options.outWidth; int h = options.outHeight; Matrix mtx = new Matrix(); mtx.postRotate(90); // Rotating Bitmap Bitmap bmp = BitmapFactory.decodeByteArray(data, 0, data.length); Bitmap rotatedBMP = Bitmap.createBitmap(bmp, 0, 0, w, h, mtx, true); return rotatedBMP; } else { return null; }}","tags":[{"name":"android","slug":"android","permalink":"http://kmfish.github.io/tags/android/"}]}]