Android-Native-内存泄漏系统化解决方案

导读:C++内存泄漏问题的分析、定位一直是Android平台上困扰开发人员的难题。因为地图渲染、导航等核心功能对性能要求很高,高德地图APP中存在大量的C++代码。解决这个问题对于产品质量尤为重要和关键,高德技术团队在实践中形成了一套自己的解决方案。 分析和定位内存泄漏问题的核心在于分配函数的统计和栈回溯。如果只知道内存分配点不知道调用栈会使问题变得格外复杂,增加解决成本,因此两者缺一不可。 Android中Bionic的malloc_debug模块对内存分配函数的监控及统计是比较完善的,但是栈回溯在Android体系下缺乏高效的方式。随着Android的发展,Google也提供了栈回溯的一些分析方法,但是这些方案存在下面几个问题: 1.栈回溯的环节都使用的libunwind,这种获取方式消耗较大,在Native代码较多的情况下,频繁调用会导致应用很卡,而监控所有内存操作函数的调用栈正需要高频的调用libunwind的相关功能。 2.有ROM要求限制,给日常开发测试带来不便。 3.用命令行或者DDMS进行操作,每排查一次需准备一次环境,手动操作,最终结果也不够直观,同时缺少对比分析。 因此,如何进行高效的栈回溯、搭建系统化的Android Native内存分析体系显得格外重要。 高德地图基于这两点做了一些改进和扩展,经过这些改进,通过自动化测试可及时发现并解决这些问题,大幅提升开发效率,降低问题排查成本。 一、栈回溯加速Android平台上主要采用libunwind来进行栈回溯,可以满足绝大多数情况。但是libunwind实现中的全局锁及unwind table解析,会有性能损耗,在多线程频繁调用情况下会导致应用变卡,无法使用。 加速原理 编译器的-finstrument-functions编译选项支持编译期在函数开始和结尾插入自定义函数,在每个函数开始插入对__cyg_profile_func_enter的调用,在结尾插入对__cyg_profile_func_exit的调用。这两个函数中可以获取到调用点地址,通过对这些地址的记录就可以随时获取函数调用栈了。 插桩后效果示例: 这里需要格外注意,某些不需要插桩的函数可以使用__attribute__((no_instrument_function))来向编译器声明。 如何记录这些调用信息?我们想要实现这些信息在不同的线程之间读取,而且不受影响。一种办法是采用线程的同步机制,比如在这个变量的读写之处加临界区或者互斥量,但是这样又会影响效率了。 能不能不加锁?这时就想到了线程本地存储,简称TLS。TLS是一个专用存储区域,只能由自己线程访问,同时不存在线程安全问题,符合这里的场景。 于是采用编译器插桩记录调用栈,并将其存储在线程局部存储中的方案来实现栈回溯加速。具体实现如下: 1.利用编译器的-finstrument-functions编译选项在编译阶段插入相关代码。 2.TLS中对调用地址的记录采用数组+游标的形式,实现最快速度的插入、删除及获取。 定义数组+游标的数据结构: typedef struct { void* stack[MAX_TRACE_DEEP]; int current;} thread_stack_t;初始化TLS中thread_stack_t的存储key: static pthread_once_t sBackTraceOnce = PTHREAD_ONCE_INIT;static void __attribute__((no_instrument_function))destructor(void* ptr) { if (ptr) { free(ptr); }}static void __attribute__((no_instrument_function))init_once(void) { pthread_key_create(&sBackTraceKey, destructor);}初始化thread_stack_t放入TLS中: get_backtrace_info() { thread_stack_t* ptr = (thread_stack_t*) pthread_getspecific(sBackTraceKey); if (ptr) return ptr; ptr = (thread_stack_t*)malloc(sizeof(thread_stack_t)); ptr->current = MAX_TRACE_DEEP - 1; pthread_setspecific(sBackTraceKey, ptr); return ptr;}3.实现__cyg_profile_func_enter和__cyg_profile_func_exit,记录调用地址到TLS中。 ...

July 15, 2019 · 1 min · jiezi

GMTC2019闲鱼基于Flutter的架构演进与创新

2012年应届毕业加入阿里巴巴,主导了闲鱼基于Flutter的新混合架构,同时推进了Flutter在闲鱼各业务线的落地。未来将持续关注终端技术的演变及趋势 Flutter的优势与挑战 Flutter是Google开源的跨端便携UI工具包,除了具有非常优秀的跨端渲染一致性,还具备非常高效的研发体验,丰富的开箱即用的UI组件,以及跟Native媲美的性能体验。由于它的众多优势,也使得Flutter成为了近些年来热门的新技术。 通过以上的特点可以看出,Flutter可以极大的加速客户端的研发效率,与此同时得到优秀的性能体验,基于我的思考,Flutter会为以下团队带来较大的收益: 中小型的客户端团队非常适合Flutter开发,不仅一端编写双端产出,还有效的解决了小团队需要双端人员(iOS:Android)占比接近1:1的限制,在项目快速推进过程中,能让整个团队的产能最大化。App在Android市场占比远高于iOS的团队,比如出海东南亚的一些App,Android市场整体占比在90%以上,通过Flutter可以将更多的人力Focus在Android市场上,同时通过在iOS端较小的投入,在结果上达到买一送一的效果。以量产App为主要策略的团队,不论是量产ToB的企业App,还是有针对性的产出不同领域的ToC的App的公司,都可以通过一端开发多端产出的Flutter得到巨大的产能提升。 闲鱼在以上的场景中属于第一种场景,服务3亿用户的闲鱼App的背后,开发资源投入很少,与竞对相比,我们是一只再小不过的团队,在这种场景下,Flutter为闲鱼业务的稳定发展以及提供更多的创新产品给予了很大的帮助。 但与此同时,Flutter在设计上带来的优势同时又会带来新的问题。所有的新技术都是脱胎于老技术的,Flutter也不例外,其身上带有很多Chrome的影子。我们再做一层简化,如果我们认为Flutter是一个使用Dart语言的浏览器容器,请大家思考一下两个问题如何解决。 如果在一个已经存在的App中加入Flutter,如何让Native与Flutter进行无缝的衔接,同时保证相互开发之间的隔离性如果在Flutter的容器中,使用已有的Native UI组件,在Flutter与Native渲染机制不同的情况下,怎么保证两者的无缝衔接以及高性能。闲鱼的架构演进与创新带着上面两个问题,我们来到闲鱼场景下的具体Case以及解决方案的演进过程。 已有App+Flutter容器 在这种情况下,闲鱼需要考虑的是首先要考虑引入Flutter容器后的内存压力,保证不要产生更多的内存溢出。与此同时我们希望能让Flutter和Native之间的页面切换是顺畅的,对不同技术栈之间的同学透明。因此我们有针对性的进行了多次迭代。 在没有任何改造的情况下以iOS为例,你可以通过创建新的FlutterViewController来创建一个新的Flutter容器,这个方案下,当创建多个FlutterViewController时会同时在内存中创建多个Flutter Engine的Runtime(虽然底层Dart VM依然只有一个),这对内存消耗是相当大的,同时多个Flutter Engine的Runtime会造成每个Runtime内的数据无法直接共享,造成数据同步困难。 这种情况下,闲鱼选择了全局共享同一个FlutterViewController的方式保证了内存占用的最小化,同时通过基础框架Flutter Boost提供了Native栈与Flutter栈的通信与管理,保证了当Native打开或关闭一个新的Flutter页面时,Dart侧的Navigator也做到自动的打开或关闭一个新的Widget。目前Google官方的提供的方案上就是参考闲鱼早先的这个版本进行的实现的。 然而在这种情况下,如果出现如闲鱼图中所示多个Tab的场景下,整个堆栈逻辑就会产生混乱,因此闲鱼在这个基础上对Flutter Boost的方案进行了升级并开源,通过在Dart侧提供一个BoostContainerManager的方式,提供了对多个Navigator的管理能力,如果打比方来看这件事,就相当于,针对Flutter的容器提供了一个类似WebView的OpenWindow的能力,每做一次OpenWindow的调用,就会产生一个新的Navigator,这样开发者就可以自由的选择是在Navigator里进行Push和Pop,还是直接通过Flutter Boost新开一个Navigator进行独立管理。 Flutter Boost目前已在github开源,由于闲鱼目前线上版本只支持Flutter 1.2的版本,因此需要支持1.5的同学等稍等,我们会在近期更新支持1.5的Flutter Boost版本。 Flutter页面+Native UI 由于闲鱼是一个闲置交易社区,因此图片和视频相对较多,对图片视频的线上性能以及内存占用有较严格的要求。目前Flutter已提供的几种方案中(Platform View以及Flutter Plugin),不论是对内存的占用还是整个的线上流畅度上还存在一定的问题,这就造成了当大部分同学跟闲鱼一样实现一个复杂的图文Feed推荐场景的时候,非常容易产生内存溢出。而实际上,闲鱼在以上的场景下有针对性的做出了较大的优化。 在整个的Native UI到Flutter渲染引擎桥接的过程中,我们选用了Flutter Plugin中提供的FlutterTextureRegistry的能力,在去年上半年我们优先针对视频的场景进行了优化,优化的思路主要是针对Flutter Engine底层的外接纹理接口进行修改,将原有接口中必须传入一个PixelBuffer的内存对象这一限制做了扩展,增加一个新的接口保证其可以传入一个GPU对象的TextureID。 如图中所示,优化后的整个链路Flutter Engine可以直接通过Native端已经生成好的TextureID进行Flutter侧的渲染,这样就将链路从Native侧生成的TextureID->copy的内存对象PixelBuffer->生成新的TextureID->渲染,转变为Native侧生成的TextureID->渲染。整个链路极大的缩短,保证了整个的渲染效率以及更小的内存消耗。闲鱼在将这套方案上线后,又尝试将该方案应用于图片渲染的场景下,使得图片的缓存,CDN优化,图片裁切等方案与Native归一,在享受已有集团中间件的性能优化的同时,也得到了更小的内存消耗,方案落地后,内存溢出大幅减少。 目前该方案由于需要配合Flutter Engine的修改,因此暂时无法提供完整的方案至开源社区,我们正在跟google积极沟通整个修改方案,相信在这一两个月内会将试验性的Engine Patch开源至社区,供有兴趣的同学参考。 复杂业务场景的架构创新实践 将以上两个问题解决以后,闲鱼开始了Flutter在业务侧的全面落地,然而很快又遇到新的问题,在多人协作过程中: 如何提供一些标准供大家进行参考保证代码的一致性如何将复杂业务进行有效的拆解变成子问题如何保证更多的同学可以快速上手并写出性能和稳定性都不错的代码 在方案的前期,我们使用了社区的Flutter Redux方案,由于最先落地的详情,发布等页面较为复杂,因此我们有针对性的对View进行了组件化的拆分,但由于业务的复杂性,很快这套方案就出现了问题,对于单个页面来说,State的属性以及Reducer的数量都非常多,当产生新需求堆叠的时候,修改困难,容易产生线上问题。 针对以上的情况,我们进行了整个方案的第二个迭代,在原有Page的基础上提供了Component的概念,使得每个Component具备完整的Redux元素,保证了UI,逻辑,数据的完整隔离,每个Component单元下代码相对较少,易于维护和开发,但随之而来的问题是,当页面需要产生数据同步时,整个的复杂性飙升,在Page的维度上失去了统一状态管理的优势。 在这种情况下闲鱼换个角度看端侧的架构设计,我们参考React Redux框架中的Connect的思想,移除掉在Component的Store,随之而来的是新的Connector作为Page和Component的数据联通的桥梁,我们基于此实现了Page State到Component State的转换,以及Component State变化后对Page State的自动同步,从而保证了将复杂业务有效的拆解成子问题,同时享受到统一状态管理的优势。与此同时基于新的框架,在统一了大家的开发标准的情况下,新框架也在底层有针对性的提供了对长列表,多列表拼接等case下的一些性能优化,保证了每一位同学在按照标准开发后,可以得到相对目前市面上其他的Flutter业务框架相比更好的性能。 目前这套方案Fish Redux已经在github开源,目前支持1.5版本,感兴趣的同学可以去github进行了解。 研发智能化在闲鱼的应用闲鱼在去年经历了业务的快速成长,在这个阶段上,我们同时进行了大量的Flutter的技术改造和升级,在尝试新技术的同时,如何能保证线上的稳定,线下的有更多的时间进行新技术的尝试和落地,我们需要一些新的思路和工作方式上的改变。 以我们日常工作为例,Flutter的研发同学,在每次开发完成后,需要在本地进行Flutter产物的编译并上传到远端Repo,以便对Native同学透明,保证日常的研发不受Flutter改造的干扰。在这个过程中,Flutter侧的业务开发同学面临着很多打包上传更新同步等繁琐的工作,一不小心就会出错,后续的排查等让Flutter前期的开发变成了开发5分钟,打包测试2小时。同时Flutter到底有没有解决研发效率快的问题,以及同学们在落地过程中有没有Follow业务架构的标准,这一切都是未知的。 在痛定思痛以后,我们认为数据化+自动化是解决这些问题的一个较好的思路。因此我们首先从源头对代码进行管控,通过commit,将代码与后台的需求以及bug一一关联,对于不符合要求的commit信息,不允许进行代码合并,从而保证了后续数据报表分析的数据源头是健康的。 在完成代码和任务关联后,通过webhook就可以比较轻松的完成后续的工作,将每次的commit有效的关联到我们的持续集成平台的任务上来,通过闲鱼CI工作平台将日常打包自动化测试等流程变为自动化的行为,从而极大的减少了日常的工作。粗略统计下来,在去年自动化体系落地的过程中单就自动打Flutter包上传以及触发最终的App打包这一流程就让每位同学每天节省一个小时以上的工作量,效果非常明显。另外,基于代码关联需求的这套体系,可以相对容易的构建后续的数据报表对整个过程和结果进行细化的分析,用数据驱动过程改进,保证新技术的落地过程的收益有理有据。 总结与展望回顾一下上下文 Flutter的特性非常适合中小型客户端团队/Android市场占比较高的团队/量产App的团队。同时由于Flutter的特性导致其在混合开发的场景下面存在一定劣势。闲鱼团队针对混合开发上的几个典型问题提供了对应的解决方案,使整个方案达到上线要求,该修改会在后续开放给google及社区。为全面推动Flutter在业务场景下的落地,闲鱼团队通过多次迭代演进出Fish Redux框架,保证了每位同学可以快速写出相对优秀的Flutter代码。新技术的落地过程中,在过程中通过数据化和自动化的方案极大的提升了过程中的效率,为Flutter在闲鱼的落地打下了坚实的基础。除了本文提及的各种方案外,闲鱼目前还在多个方向上发力,并对针对Flutter生态的未来进行持续的关注,分享几个现在在做的事情 Flutter整个上层基础设施的标准化演进,混合工程体系是否可以在上层完成类似Spring-boot的完整体系构架,帮助更多的Flutter团队解决上手难,无行业标准的问题。动态性能力的扩展,在符合各应用商店标准的情况下,助力业务链路的运营效率提升,保证业务效果。目前闲鱼已有的动态化方案会后续作为Fish-Redux的扩展能力提供动态化组件能力+工具链体系。Fish-Redux + UI2Code,打通代码生成链路和业务框架,保证在团队标准统一的情况下,将UI工作交由机器生成。Flutter + FaaS,让客户端同学可以成为全栈工程师,通过前后端一体的架构设计,极大的减少协同,提升效率。让工程师去从事更多创造性的工作,是我们一直努力的目标。闲鱼团队也会在新的一年更多的完善Flutter体系的建设,将更多已有的沉淀回馈给社区,帮助Flutter社区一起健康成长。 ...

June 26, 2019 · 1 min · jiezi

揭秘一个高准确率的Flutter埋点框架如何设计

背景用户行为埋点是用来记录用户在操作时的一系列行为,也是业务做判断的核心数据依据,如果缺失或者不准确将会给业务带来不可恢复的损失。闲鱼将业务代码从Native迁移到Flutter上过程中,发现原先Native体系上的埋点方案无法应用在Flutter体系之上。而如果我们只把业务功能迁移过来就上线,对业务是极其不负责任的。因此,经过不断探索,我们沉淀了一套Flutter上的高准确率的用户行为埋点方案。 用户行为埋点定义先来讲讲在我们这里是如何定义用户行为埋点的。在如下用户时间轴上,用户进入A页面后,看到了按钮X,然后点击了这个按钮,随即打开了新的页面B。 这个时间轴上有如下5个埋点事件发生: 进入A页面。A页面首帧渲染完毕,并获得了焦点。曝光坑位X。按钮X处于手机屏幕内,且停留一段时间,让用户可见可触摸。点击坑位X。用户对按钮X的内容很感兴趣,于是点击了它。按钮X响应点击,然后需要打开一个新页面。离开A页面。A页面失去焦点。进入B页面。B页面首帧渲染完毕,并获得焦点。在这里,打埋点最重要的是时机,即在什么时机下的事件中触发什么埋点,下面来看看闲鱼在Flutter上的实现方案。 实现方案进入/离开页面 在Native原生开发中,Android端是监听Activity的onResume和onPause事件来做为页面的进入和离开事件,同理iOS端是监听UIViewController的viewWillAppear和viewDidDisappear事件来做为页面的进入和离开事件。同时整个页面栈是由Android和iOS操作系统来维护。 在Flutter中,Android和iOS端分别是用FlutterActivity和FlutterViewController来做为容器承载Flutter的页面,通过这个容器可以在一个Native的页面内(FlutterActivity/FlutterViewController)来进行Flutter原生页面的切换。即在Flutter自己维护了一个Flutter页面的页面栈。这样,原来我们最熟悉的那套在Native原生上方案在Flutter上无法直接运作起来。 针对这个问题,可能很多人会想到去注册监听Flutter的NavigatorObserver,这样就知道Flutter页面的进栈(push)和出栈(pop)事件。但是这会有两个问题: 假设A、B两个页面先后进栈(A enter -> A leave -> B enter)。然后B页面返回退出(B leave),此时A页面重新可见,但是此时是收不到A页面push(A enter)的事件。假设在A页面弹出一个Dialog或者BottomSheet,而这两类也会走push操作,但实际上A页面并未离开。好在Flutter的页面栈不像Android Native的页面栈那么复杂,所以针对第一个问题,我们可以来维护一个和页面栈匹配的索引列表。当收到A页面的push事件时,往队列里塞一个A的索引。当收到B页面的push事件时,检测列表内是否有页面,如有,则对列表最后一个页面执行离开页面事件记录,然后再对B页面执行进入页面事件记录,接着往队列里塞一个B的索引。当收到B页面的pop事件时,先对B页面执行离开页面事件记录,然后对队列里存在的最后一个索引对应的页面(假设为A)进行判断是否在栈顶(ModalRoute.of(context).isCurrent),如果是,则对A页面执行进入页面事件记录。 针对第二个问题,Route类内有个成员变量overlayEntries,可以获取当前Route对应的所有图层OverlayEntry,在OverlayEntry对象中有个成员变量opaque可以判断当前这个图层是否全屏覆盖,从而可以排除Dialog和BottomSheet这种类型。再结合问题1,还需要在上述方案中加上对push进来的新页面来做判断是否为一个有效页面。如果是有效页面,才对索引列表中前一个页面做离开页面事件,且将有效页面加到索引列表中。如果不是有效页面,则不操作索引列表。 以上并不是闲鱼的方案,只是笔者给出的一个建议。因为闲鱼APP在一开始落地Flutter框架时,就没有使用Flutter原生的页面栈管理方案,而是采用了Native+Flutter混合开发的方案。具体可参考前面的一篇文章《已开源|码上用它开始Flutter混合开发——FlutterBoost》。因此接下来也是基于此来阐述闲鱼的方案。 闲鱼的方案如下(以Android为例,iOS同理): 注:首次打开指的是基于混合栈新打开一个页面,非首次打开指的是通过回退页面的方式,在后台的页面再次到前台可见。 看似我们将何时去触发进入/离开页面事件的判断交给Flutter侧,实际上依然跟Native侧的页面栈管理保持了一致,将原先在Native侧做打埋点的时机告知Flutter侧,然后Flutter侧再立刻通过channel来调用Native侧的打埋点方法。那么可能会有人问,为什么这么绕,不全部交给Native侧去直接管理呢?交给Native侧去直接管理这样做针对非首次打开这个场景是合适的,但是对首次打开这个场景却是不合适的。因为在首次打开这个场景下,onResume时Flutter页面尚未初始化,此时还不知道页面信息,因此也就不知道进入了什么页面,所以需要在Flutter页面初始化(init)时再回过来调Native侧的进入页面埋点接口。为了避免开发人员去关注是否为首次打开Flutter页面,因此我们统一在Flutter侧来直接触发进入/离开页面事件。 曝光坑位先讲下曝光坑位在我们这里的定义,我们认为图片和文本是有曝光意义的,其他用户看不见的是没有曝光意义的,在此之上,当一个坑位同时满足以下两点时才会被认为是一次有效曝光: 坑位在屏幕可见区域中的面积大于等于坑位整体面积的一半。坑位在屏幕可见区域中停留超过500ms。基于此定义,我们可以很快得出如下图所示的场景,在一个可以滚动的页面上有A、B、C、D共4个坑位。其中: 坑位A已经滑出了屏幕可见区域,即invisible;坑位B即将向上从屏幕中可见区域滑出,即visible->invisible;坑位C还在屏幕中央可视区域内,即visible;坑位D即将滑入屏幕中可见区域,invisible->visible; 那么我们的问题就是如何算出坑位在屏幕内曝光面积的比例。要算出这个值,需要知道以下几个数值: 容器相对屏幕的偏移量坑位相对容器的偏移量坑位的位置和宽高容器的位置和宽高其中坑位和容器的宽和高很容易获取和计算,这里就不再累述。 获取容器相对屏幕的偏移量 //监听容器滚动,得到容器的偏移量double _scrollContainerOffset = scrollNotification.metrics.pixels;获取坑位相对容器的偏移量 //曝光坑位Widget的contextfinal RenderObject childRenderObject = context.findRenderObject();final RenderAbstractViewport viewport = RenderAbstractViewport.of(childRenderObject);if (viewport == null) { return;}if (!childRenderObject.attached) { return;}//曝光坑位在容器内的偏移量final RevealedOffset offsetToRevealTop = viewport.getOffsetToReveal(childRenderObject, 0.0);逻辑判断 if (当前坑位是invisible && 曝光比例 >= 0.5) { 记录当前坑位是visible状态 记录出现时间} else if (当前坑位是visible && 曝光比例 < 0.5) { 记录当前坑位是invisible状态 if (当前时间-出现时间 > 500ms) { 调用曝光埋点接口 }}点击坑位 ...

June 13, 2019 · 1 min · jiezi

一个优秀的可定制化Flutter相册组件看这一篇就够了

背景在做图片、视频相关功能的时候,相册是一个绕不开的话题,因为大家基本都有从相册获取图片或者视频的需求。最直接的方式是调用系统相册接口,基本功能是满足的,一些高级功能就不行了,例如自定义UI、多选图片等。 我们调研了官方的image_picker,它也是调用系统的相册接口来处理的,可定制程度不高,不能满足我们的要求。所以我们选择自己来开发Flutter相册组件。 我们的组件需要有如下的功能: 在app内完成图片、视频的选取,完全不用依赖系统相册组件可以多选图片,支持指定选定图片的总数目在多选的时候UI反应出选择的序号。可以控制视频、图片的选择。例如:只让用户选择视频,图片是灰色的。大图预览的时候可以放大缩小,也可直接加入到选取列表。设计思路API使用简单,功能丰富灵活,具有较高的订制性。业务方可以选择完全接入组件,也可以选择在组件上面进行UI定制。 Flutter做UI展现层,具体的数据由各Native平台提供。这种模式,天然从工程上把UI代码和数据代码进行了隔离。我们在开发一个native组件的时候常常会使用MVC架构。Flutter组件的开发的思路也基本类似。整体架构如下: 可以看出,在Flutter侧是一个典型的MVC架构,这里Widget就是View,View和Model绑定,在Model改变的时候View会重新build反映出Model的变化。View的事件会触发Controller去Native获取数据然后更新Model。Native和Flutter通过Method Channel进行通信,两层之间没有强依赖关系,只需要按约定的协议进行通信即可。 Native侧的组成部分,UIAdapter主要是负责机型的适配、刘海屏、全面屏之类的识别。Permission负责媒体读写权限的申请处理。Cache主要负责缓存GPU纹理,在大图预览的时候提高响应速度。Decoder负责解析Bitmap,OpenGL负责Bitmap转纹理。 需要说明的是:我们的这一套实现依赖于flutter外接纹理。在整个相册组件看到的大多数图片都是一个GPU纹理,这样给java堆内存的占用相对于以前的相册实现有大幅的降低。在低端机上面如果使用原生的系统相册,由于内存的原因,app有被系统杀掉的风险。现象就是,从系统相册返回,app重新启动了。使用Flutter相册组件,在低端机上面体验会有所改观。 一些细节1. 分页加载 相册列表需要加载大量图片,Flutter的GridView组件有好几个构造函数,比较容易犯的错误是使用了第一个函数,这需要在一开始就提供大量的widget。应该选择第二个构造函数,GridView在滑动的时候会回调IndexedWidgetBuilder来获取widget,相当于一种懒加载。 GridView.builder({ ... List<Widget> children = const <Widget>[], ... })GridView.builder({ ... @required IndexedWidgetBuilder itemBuilder, int itemCount, ... })滑动过程中,图片滑过后,也就是不可见的时候要进行资源的回收,我们这里这里对应的就是纹理的删除。不断的滑动GridView,内存在上升后会处于稳定,不会一直增长。如果快速的来回滑动纹理会反复的创建和删除,这样会有内存的抖动,体验不是很好。 于是,我们维护了一个图片的状态机,状态有None,Loading,Loaded,Wait_Dispose,Disposed。开始加载的时候,状态从None进入Loading,这个时候用户看到的是空白或者是占位图,当数据回调回来会把状态设置为Loaded的这时候会重新build widget树来显示图片icon,当用户滑走的时候状态进入 Wait_Dispose,这时候并不会马上Dispose,如果用户又滑回来则会从Wait_Dispose进入Loaded状态,不会继续Dispose。如果用户没有往回滑则会从Wait_Dispose进入Disposed状态。当进入Disposed状态后,再需要显示该图片的时候就需要重新走加载流程了。 2. 相册大图展示: 当点击GridView的某张图片的时候会进行这张图片的大图展示,方便用户查看的更清楚。我们知道相机拍摄的图片分辨率都是很高的,如果完全加载,内存会有很大的开销,所以我们在Decode Bitmap的时候进行了缩放,最高只到1080p。大图展示可以概括为三个步骤。 1 从文件Decode出Bitmap2 Bitmap转换成为纹理,并释放Bitmap3 纹理交给Flutter进行展示在步骤1中,Android原生的Bitmap Decode经验同样适用,先Decode出Bitmap的宽高,然后根据要展示的大小计算出缩放倍数, 然后Decode出需要的Bitmap。 Android相册的图片大多是有旋转角度的,如果不处理直接显示,会出现照片旋转90度的问题,所以需要对Bitmap进行旋转,采用Matrix旋转一张1080p的图片在我的测试机器上面大概需要200ms,如果使用OpenGL的纹理坐标进行旋转,大于只需要10ms左右,所以采用OpenGl进行纹理的旋转是一个较好的选择。 在进行大图预览的时候会进入一个水平滑动的PageView,Flutter的PageView一般来说是不会去主动加载相邻的page的。举个例子,在显示index是5的page的时候index为4,6的page也不会提前创建的。这里有一个取巧的办法,对于PageController的viewportFraction参数我们可以设置成为0.9999。对于前面这个例子,就是在显示index是5的page的时候,index为4,6的page也需要显示0.0001。这样index为4,6的page显示不到1个像素,基本上看不出来: PageController(viewportFraction=0.9999)还有另外一种办法,就是在Native侧做预加载。例如:在加载第5张图片的时候,相邻的4,6的图片纹理提前进行加载,当滑动到4,6的时候直接使用缓存的纹理。 纹理缓存后,一个直接的问题:什么时候释放纹理?等到预览页面退出的时候释放所有的纹理显示不是很合适,如果用户一直浏览内存则会无限增长。所以,我们维护了一个5个纹理的LRU缓存,在滑动过程中,最老的纹理会被释放掉。在页面退出的时候整个LRU的缓存会进行销毁。 3. 关于内存 相册图片使用GPU纹理,会大幅减少Java堆内存的占用,对整个app的性能有一定的提升。需要注意的是,GPU的内存是有限的需要在使用完毕后及时删除,不然会有内存的泄漏的风险。另外,在Android平台删除纹理的时候需要保证在GPU线程进行,不然删除是没有效果的。 在华为P8,Android5.0上面进行了对比测试,Flutter相册和原native相册总内存占用基本一致,在GridView列表页面,新增最大内存13M左右。它们的区别在于原native相册使用的是Java堆内存,Flutter相册使用的是Native内存。 总结相册组件API简单、易用,高度可定制。Flutter侧层次分明,有UI订制需求的可以重写Widget来达到目的。另外这是一个不依赖于系统相册的相册组件,自身是完备的,能够和现有的app保持UI、交互的一致性。同时为后面支持更多和相册相关的玩法打好基础。 后续计划由于我们使用的是GPU纹理,可以考虑支持显示高清4K图片,而且客户端内存不会有太大的压力。但是4k图片的Bitmap转纹理需消耗更多的时间,UI交互上面需要做些loading状态的支持。 组件功能丰富,稳定后,进行开源,回馈给社区。 本文作者:闲鱼技术-邻云阅读原文 本文为云栖社区原创内容,未经允许不得转载。

May 31, 2019 · 1 min · jiezi

走近科学探究阿里闲鱼团队通过数据提升Flutter体验的真相

背景闲鱼客户端的flutter页面已经服务上亿级用户,这个时候Flutter页面的用户体验尤其重要,完善Flutter性能稳定性监控体系,可以及早发现线上性能问题,也可以作为用户体验提升的衡量标准。那么Flutter的性能到底如何?是否像官方宣传的那么丝滑?Native的性能指标是否可以用来检测Flutter页面?下面给大家分享我们在实践中总结出来的Flutter的性能稳定性监控方案。 目标过度的丢帧从视觉上会出现卡顿现象,体现在用户滑动操作不流畅;页面加载耗时过长容易中断操作流程;Flutter部分exception会导致发生异常代码后面的逻辑没有走到从而造成逻辑bug甚至白屏。这些问题很容易考验用户耐心,引起用户反感。 所以我们制定以下三个指标作为线上Flutter性能稳定性标准: 页面滑动流畅度页面加载耗时(首屏时长+可交互时长)Exception率最终目标是让这些数据指标驱动Flutter用户体验升级。 页面滑动流畅度我们先大概了解下屏幕渲染流程:CPU先把UI对象转变GPU可以识别的信息存储进displaylist列表,GPU执行绘图指令来执行displaylist,取出相应的图元信息,进行栅格化渲染,显示到屏幕上,这样一个循环的过程实现屏幕刷新。 闲鱼客户端采用的Native、Flutter混合技术方案,Native页面FPS监控采用集团高可用方案,Flutter页面是否可以直接采用这套方案监控? 普遍的FPS检测方案Android端采用的是Choreographer.FrameCallBack,IOS采用的是CADisplayLink注册的回调,原理是类似的,在每次发出Vsync信号,并且CPU开始计算的时候执行到对应的回调,这个时候表示屏幕开始一次刷新,计算固定时间内屏幕渲染次数来得到fps。(这种方式只能检测到CPU卡顿,对于GPU的卡顿是无法监控到的)。由于这两种方法都是在主线程做检测处理,而flutter的屏幕绘制是在UI TaskRunner中进行,真正的渲染操作是在GPU TaskRunner中,关于详细的Flutter线程问题可以参考闲鱼之前的文章:深入理解Flutter引擎线程模式。 这里我们得出结论:Native的FPS检测方法并不适用于Flutter。 Flutter官方给我们提供了 Performance Overlay (具体参考 Flutter performance profiling) 作为检测帧率工具,可否直接拿来用? 上图显示了Performance Overlay模式下的帧率统计,可以看到,Flutter分开计算GPU 和UI TaskRunner。UI Task Runner被Flutter Engine用于执行Dart root isolate代码,GPU Task Runner被用于执行设备GPU的相关调用。通过对flutter engine源码分析,UI frame time是执行window.onBeginFrame所花费的总时间。GPU frame time是处理CPU命令转换为GPU命令并发送给GPU所花费的时间。 这种方式只能在debug和profile模式下开启,没有办法作为线上版本的fps统计。但是我们可以通过这种方式获得启发,通过监听Flutter页面刷新回调方法handleBeginFrame()、handleDrawFrame()来计算实际FPS。 具体实现方式:注册WidgetsFlutterBinding监听页面刷新回调handleBeginFrame()、handleDrawFrame() handleBeginFrame: Called by the engine to prepare the framework to produce a new frame.handleDrawFrame: Called by the engine to produce a new frame.通过计算handleBeginFrame和handleDrawFrame之间的时间间隔计算帧率,主要流程如下图: 效果到这里,我们完成Flutter中页面帧率的统计,这种方式统计的是UI TaskRunner中的CPU操作耗时,GPU操作在Flutter引擎内部实现,要修改引擎来监控完整的渲染耗时,我们目前大部分的场景没有复杂到gpu卡顿,问题主要还是集中在CPU,所以说可以反应出大部分问题。从线上数据来看,release模式下Flutter的流畅度还是蛮不错的,ios的主要页面均值基本维持在50fps以上,android相对ios略低。这里需要注意的是帧率的均值fps在反复滑动过程中会有一个稀释效果,导致一些卡顿问题没有暴露出来,所以除了fps均值,需要综合掉帧范围、卡顿秒数、滑动时长等数据才能反应出页面流畅度情况。 页面加载时长集团内部高可用方案统计Native页面加载时长是通过容器初始化后开启定时器在容器layout的时候检查屏幕渲染程度,计算可见组件的屏幕覆盖率,满足条件水平>60%,垂直>80%以上认为满足页面填充程度,再检查主线程心跳判断是否加载完成 再来看看weex页面加载流程和统计数据的定义 ...

April 26, 2019 · 1 min · jiezi

flutter在2019年会有怎样的表现?

Flutter的趋势在移动端,受成本和效率的驱使,跨平台一站式开发慢慢成为一个趋势。从Hybird,RN,WEEX,Flutter,到各种小程序或快应用的大量涌现,虽然很多跨平台方案都有各自的优缺点,目前还没有完美无缺的终极方案,但这已是未来移动端开发不可逆转的一大方向。而Google推出并开源的移动应用开发框架Flutter,更是其中的明星。笔者从自身在做Flutter相关的分享中,特别强烈的感受是,有非常非常多的Native技术栈的同学在学习和使用Flutter,有非常多的前端技术栈的同学在时刻关注Flutter的hummingbird和desktop-embedding的进展。尤其自Flutter1.0 发布后,Flutter受到了业界更多的关注和期待。跨平台解决方案比较目前几个主流的跨平台解决方案:基于浏览器技术的Hybird基于桥接Native组件,如RN、WEEX基于底层统一渲染,如Flutter它们有各种的优缺点,但浏览器技术无疑是其中的历史最长、标准最完善、用户最多、生态最丰富的。RN、WEEX也可以归类为javascript生态的一个小分支。而Flutter走的是和前两者截然不同的路线,它是一个新兴的挑战者,通过底层统一渲染,得到高度一致的跨端效果;通过引入dart,得到AOT的接近原生的性能,和JIT的快速开发体验;通过上层完善的组件体系(material design & cupertino),得到高保真的UI体验。但它也并非尽善尽美。同时基于底层统一渲染的跨平台方案有很多,在移动端有实际应用的如QT、cocos2d等。对比Flutter和QT,最大的区别在语言和背后团队。语言:Flutter选择了Dart,QT是C++。Dart相比C++,对开发者来说无疑于相比骑自行车和开飞机的区别,Dart更容易编写,除此以外,Dart还拥有AOT和JIT两种模式、类型安全、快速内存分配等等特点,确实如Flutter团队所述,同时拥有一两条这些优点的语言不少,但是将所有这些优点集于一身的,只有Dart。背后团队:Flutter的背后是Google,QT的背后是TrollTech,从社区影响力和号召力而言不可同日而语。但同时也必须要认识到的是通过底层统一渲染的跨平台方案,也有它天然的劣势。它很难复用系统天然提供的组件。在摆脱对操作系统的依赖和复用操作系统的能力上,要考虑如何达到了一个最佳的平衡。Flutter的生态如果拿Flutter生态同React和Native进行比较的话基于核心UI表达层向上,这一层会更接近前端的体系,以React生态为参照物,主要的几部分路由体系一种面向以Flutter为主的应用,它的路由以Flutter为主,Native的路由部分往往以简易桥接的形态存在。一种面向混合技术栈为主的应用,它的路由以Native为主,Flutter为辅。状态管理体系 | 应用框架基本上在React生态下有的状态管理,Flutter也有,同时有一些是Flutter独有的。开源的代表有:flutter_redux, google的BLoc,scoped_model,及闲鱼的fish-redux,它在真实的复杂场景下得到了非常好的验证。UI库体系目前已有不少UI库,包含常见的组件。基于核心UI表达层向下,这一层会更接近Native的体系,以Native生态为参照物,主要的几部分核心的一些基础中间件,如网络,图片,音视频,存储,埋点,监控等。目前和Native相比还是有非常大的差距。所以也导致了目前大部分这些问题的解决方案,都趋向于桥接的形态。通过复用Native能力来短期补齐Flutter能力不足的。但它不一定是未来的最佳的方案。一些重量级的基础组件,如WebView,MapView等。目前已经能通过PlatformView的形式,得到能力拓展。但是它有使用的局限性和性能上的损失。Flutter今年几个重要的突破点Code-Push在当下国内应用生态环境,热修复或者热部署能力在很多公司和团队做技术选型中,往往是其中非常重要的一个选项。如果有Google官方推出,不管是hotfix,还是dynamic-boundle都将极大的推动Flutter在国内的发展。而基于dart语言的特性判断,在Flutter上做code-push理论上会比目前任何Native的code-push方案有更强的能力。闲鱼团队一直和Flutter团队就这方便保持紧密的联系,在之前的验证中,目前在android端是可以支持的,但还留有一些瑕疵。Humming-Bird在跨平台之外,还有一层更高级别的诉求,多应用投放,打破应用之间的孤岛壁垒,实现更多的商业价值。而要完成多应用的投放,首选的是基于浏览器的方案。Humming-Bird方案为这样的设想,提供了可能。同时Humming-Bird也将大大扩张了Flutter的边界,吸引更多的开发者和厂商的加入,同时让面向终端的全栈解决方案成为可能。Dart语言也有可能成为javascript生态的更好的补充和演进。Flutter面向未来基础架构设计决定了一个软件的发展上限,它带来了更多的想象力。使用Flutter和Dart,既是Google为摆脱和Oracle纠缠多年的“Java 侵权案”提前下的一颗棋,也是Google为下一代操作系统Fusion下的一颗棋,是即Google通过chromium项目渐进的统一浏览器领域,着眼于更多的终端,为了一个更大终端生态的大一统做准备。这让Flutter和Dart充满了更高层次的可能。如果没有这些可能,Flutter的生命无疑是会短暂的,因为它还未能建立被广泛被认可的标准,就像我们终端里走过的那么多的技术一样,都是有限的解决了当下的诉求,但随着终端的更替,操作系统的演进,慢慢变成了明日黄花。而正是这些更多的可能,是Flutter持续演进的源泉,是Flutter相比其他的跨平台方案中最吸引人的部分。本文作者:闲鱼技术-吉丰阅读原文本文为云栖社区原创内容,未经允许不得转载。

April 19, 2019 · 1 min · jiezi

开发跨平台app推荐React Native还是flutter?

嗯。。。这个问题十分不好回答啊(捋下鱼须)。闲鱼作为flutter领域的先驱者,以及fish_redux、flutter_boost等当红flutter库的作者,当然是欢迎广大的开发者多多使用flutter相关技术栈 逃~:)。咳咳,不过呢,我们还是正经得聊一下React Native(下面简称RN)和flutter之前的异同:0x00 简单介绍一下React NativeReact Native是Facebook开源的一款基于react思想、使用JS、能够给移动平台带来native般体验的框架,官网最新的版本是0.5.9。flutterflutter来自Google,上层使用dart语言构建跨平台应用,通过平台相关的embedded层接入到使用c++编写的engine层,再通过skia库直接与GPU进行交互。通过对dart代码的AOT编译,拥有优异的计算(CPU)、渲染(GPU)性能。官网最新的版本为1.2。0x01 跨平台性开发者们使用跨平台技术栈,首要的目的是为了能够省事儿,所以跨平台能力是首先要被衡量的指标。Build native mobile apps using JavaScript and React这意味着开发者可以复用庞大的JavaScript生态和优雅的react思想来书写RN的代码,给开发提供很多的便利性。从实现原理上来说,RN进行完排版之后会把最终的渲染交给native view,这种方式带来的是如native般的UI性能,但同时也给给平台一致性带来了一些问题。除开渲染层的不一致,在iOS和Android没有使用同一个JavaScript虚拟机也会带来一些暗坑。手势的处理上两个平台不好统一,RN官方也没有提供一个抹平差异的库,虽然开源社区有react-native-gesture-handler。Beautiful native apps in record timeflutter官方的口气很大,说自己是”空前“的。是不是”空前“,我们得来评估一下。编程语言层面,flutter使用dart语言构建应用,这门语言对大多数人来说应该是比较陌生,好在dart的语法并不复杂,与Java等强类型oop语言非常相似,还加入了函数式的特性,使用起来还是挺方便的。flutter提供类似React思想的响应性UI编程模型,让UI开发变得更加fancy。原理上来说,flutter在各个平台上使用统一的vm(dart vm),自带GDI(skia)。skia是一个已经发展多年成熟度相当高的2D图形库,也是Android系统和Chrome一直在使用的图形库。flutter从逻辑计算到渲染绘图,都是自己的,使得它在跨平台一致性上有良好的表现。dart提供的AOT特性也可以保证应用在线上有一个好的性能表现。多平台支持RN目前支持iOS和Android两个平台,另外有个非官方的ReactNativeX的项目旨在让RN运行到其他平台。flutter早期支持iOS和Android,desktop的支持目前尚不完善。近期flutter团队发布了Hummingbird,旨在让flutter编写的应用可以运行在浏览器端。从多平台支持的角度看,两边差距不大。相比RN,flutter在desktop的支持上有些优势,但目前都是不怎么可用状态。0x02 开发便利性工具链RN在打包发布方面有被前端广泛使用的webpack支持,官方自己提供了基于浏览器的debug工具,与前端同学管用的调试方式并无二致。flutter基于iOS和Android已有的打包工具添加了flutter产物打包功能,同样debug工具也由官方自己提供,除了刚发布的基于浏览器的调试工具外,flutter团队提供的调试工具可以直接在Android Studio或者VScode这类IDE上直接使用。调试便利性JS的调试方式已经很成熟了,这里不多做展开。flutter在debug阶段可以使用集成于IDE插件中的hot reload功能做到亚秒级的新代码加载速度,十分适合与设计师坐在一起结(ya)对(li)编(tiao)程(shi) :)。第三方库在RN上你可以使用JS的大部分库,平台相关的plugin也相对丰富。flutter在这方面稍显欠缺,库的数量上无法与JS生态相比较。flutter/plugins项目提供了大量的平台相关插件供开发者使用,倒也是满足了日常开发的需求,另外dart pubs上的公开库数量也日趋上升。在混合开发和大型app业务框架上,闲鱼技术开源的flutter_boost提供了与native混合开发的可能,而fish_redux使得大型app中的复杂页面的开发在flutter中变得更加容易。0x03 未来的发展开发者选择一个技术,都是压了”身家性命“在上面,谁也不想刚入门就发现这门技术即将被淘汰。RN是个很好的项目,在发布之初给移动开发带来了一阵旋风。但不得不说,Airbnb宣布放弃使用RN技术栈对于整个社区有不小的打击,而文章中对原因的阐述也相当有说服力。flutter在1.0发布之后趋于成熟,被钦定为Google Fuchsia系统的应用层框架。从团队2019 roadmap中可以看到,flutter当前重点在于完善一些现有功能上的细节与bugfix,另外对于广受期待的动态化特性,flutter团队也在开发code push功能。从flutter团队目前的方向和笔者在闲鱼开发中实际使用的flutter的感受来看,整体上flutter在框架层面目前已经基本上稳定。从桌面端跨平台框架发展的历程来看,Java GUI从最初使用peer(对等设计模式)的AWT,到基于Java图形绘制接口性能巨慢无比的Swing,再到公认性能最好目前应用最广泛的基于目标平台绘制接口的SWT,我们可以从中窥见一些历史规律。peer(对等设计模式),即AWT中的一个控件,对应目标平台(如Windows)上的一个控件(是不是看起来跟RN有一些相似),最终AWT被放弃是因为peer模式传输层级过多造成效率低下,GUI部分为了保证可移植性只能保留各个平台公共的接口。SWT与QT(另一个被广泛使用的桌面端跨平台GUI框架),牺牲了一部分可移植性(主要是因为直接调用了目标平台的图形绘制接口),带来了GUI的高性能。flutter也采用了类似技术栈,skia来抹平各个平台的绘制接口差异,并向上提供统一的图形接口。从这个角度来说,无疑flutter可能会是一个更有未来的跨平台框架。0x04 写在最后当然Facebook官方对于RN正在进行重构,包括把大部分逻辑移动到c++层来减少线程切换的开销提升性能等。选择一个框架需要考虑的实际情况比框架的优劣比较更加重要,比如你的项目大小、开发人员构成等,RN和flutter作为目前移动平台上炙手可热的框架,两者并不是孰优孰劣的对立关系。纸上得来终觉浅,如果你是个对新技术感兴趣,抑或是希望在移动平台上有所突破的开发者,何不尝试一下Google最新的成果咧?本文作者:闲鱼技术-海猪阅读原文本文为云栖社区原创内容,未经允许不得转载。

April 19, 2019 · 1 min · jiezi

码上用它开始Flutter混合开发——FlutterBoost

开源地址: https://github.com/alibaba/fl…为什么需要混合方案具有一定规模的App通常有一套成熟通用的基础库,尤其是阿里系App,一般需要依赖很多体系内的基础库。那么使用Flutter重新从头开发App的成本和风险都较高。所以在Native App进行渐进式迁移是Flutter技术在现有Native App进行应用的稳健型方式。闲鱼在实践中沉淀出一套自己的混合技术方案。在此过程中,我们跟Google Flutter团队进行着密切的沟通,听取了官方的一些建议,同时也针对我们业务具体情况进行方案的选型以及具体的实现。官方提出的混合方案基本原理Flutter技术链主要由C++实现的Flutter Engine和Dart实现的Framework组成(其配套的编译和构建工具我们这里不参与讨论)。Flutter Engine负责线程管理,Dart VM状态管理和Dart代码加载等工作。而Dart代码所实现的Framework则是业务接触到的主要API,诸如Widget等概念就是在Dart层面Framework内容。一个进程里面最多只会初始化一个Dart VM。然而一个进程可以有多个Flutter Engine,多个Engine实例共享同一个Dart VM。我们来看具体实现,在iOS上面每初始化一个FlutterViewController就会有一个引擎随之初始化,也就意味着会有新的线程(理论上线程可以复用)去跑Dart代码。Android类似的Activity也会有类似的效果。如果你启动多个引擎实例,注意此时Dart VM依然是共享的,只是不同Engine实例加载的代码跑在各自独立的Isolate。官方建议引擎深度共享在混合方案方面,我们跟Google讨论了可能的一些方案。Flutter官方给出的建议是从长期来看,我们应该支持在同一个引擎支持多窗口绘制的能力,至少在逻辑上做到FlutterViewController是共享同一个引擎的资源的。换句话说,我们希望所有绘制窗口共享同一个主Isolate。但官方给出的长期建议目前来说没有很好的支持。多引擎模式我们在混合方案中解决的主要问题是如何去处理交替出现的Flutter和Native页面。Google工程师给出了一个Keep It Simple的方案:对于连续的Flutter页面(Widget)只需要在当前FlutterViewController打开即可,对于间隔的Flutter页面我们初始化新的引擎。例如,我们进行下面一组导航操作:Flutter Page1 -> Flutter Page2 -> Native Page1 -> Flutter Page3 我们只需要在Flutter Page1和Flutter Page3创建不同的Flutter实例即可。这个方案的好处就是简单易懂,逻辑清晰,但是也有潜在的问题。如果一个Native页面一个Flutter页面一直交替进行的话,Flutter Engine的数量会线性增加,而Flutter Engine本身是一个比较重的对象。多引擎模式的问题冗余的资源问题.多引擎模式下每个引擎之间的Isolate是相互独立的。在逻辑上这并没有什么坏处,但是引擎底层其实是维护了图片缓存等比较消耗内存的对象。想象一下,每个引擎都维护自己一份图片缓存,内存压力将会非常大。插件注册的问题。插件依赖Messenger去传递消息,而目前Messenger是由FlutterViewController(Activity)去实现的。如果你有多个FlutterViewController,插件的注册和通信将会变得混乱难以维护,消息的传递的源头和目标也变得不可控。Flutter Widget和Native的页面差异化问题。Flutter的页面是Widget,Native的页面是VC。逻辑上来说我们希望消除Flutter页面与Naitve页面的差异,否则在进行页面埋点和其它一些统一操作的时候都会遇到额外的复杂度。增加页面之间通信的复杂度。如果所有Dart代码都运行在同一个引擎实例,它们共享一个Isolate,可以用统一的编程框架进行Widget之间的通信,多引擎实例也让这件事情更加复杂。因此,综合多方面考虑,我们没有采用多引擎混合方案。现状与思考前面我们提到多引擎存在一些实际问题,所以闲鱼目前采用的混合方案是共享同一个引擎的方案。这个方案基于这样一个事实:任何时候我们最多只能看到一个页面,当然有些特定的场景你可以看到多个ViewController,但是这些特殊场景我们这里不讨论。我们可以这样简单去理解这个方案:我们把共享的Flutter View当成一个画布,然后用一个Native的容器作为逻辑的页面。每次在打开一个容器的时候我们通过通信机制通知Flutter View绘制成当前的逻辑页面,然后将Flutter View放到当前容器里面。老方案在Dart侧维护了一个Navigator栈的结构。栈数据结构特点就是每次只能从栈顶去操作页面,每一次在查找逻辑页面的时候如果发现页面不在栈顶那么需要往回Pop。这样中途Pop掉的页面状态就丢失了。这个方案无法支持同时存在多个平级逻辑页面的情况,因为你在页面切换的时候必须从栈顶去操作,无法再保持状态的同时进行平级切换。举个例子:有两个页面A,B,当前B在栈顶。切换到A需要把B从栈顶Pop出去,此时B的状态丢失,如果想切回B,我们只能重新打开B之前页面的状态无法维持住。这也是老方案最大的一个局限。如在pop的过程当中,可能会把Flutter 官方的Dialog进行误杀。这也是一个问题。而且基于栈的操作我们依赖对Flutter框架的一个属性修改,这让这个方案具有了侵入性的特点。这也是我们需要解决的一个问题。具体细节,大家可以参考老方案开源项目地址:https://github.com/alibaba-flutter/hybrid_stack_manager新一代混合技术方案 FlutterBoost重构计划在闲鱼推进Flutter化过程当中,更加复杂的页面场景逐渐暴露了老方案的局限性和一些问题。所以我们启动了代号FlutterBoost(向C++ Boost致敬)的新混合技术方案。这次新的混合方案我们的主要目标有:可复用通用型混合方案支持更加复杂的混合模式。比如支持主页Tab这种情况无侵入性方案:不再依赖修改Flutter的方案支持通用页面生命周期统一明确的设计概念跟老方案类似,新的方案还是采用共享引擎的模式实现。主要思路是由Native容器Container通过消息驱动Flutter页面容器Container,从而达到Native Container与Flutter Container的同步目的。我们希望做到Flutter渲染的内容是由Naitve容器去驱动的。简单的理解,我们想做到把Flutter容器做成浏览器的感觉。填写一个页面地址,然后由容器去管理页面的绘制。在Native侧我们只需要关心如果初始化容器,然后设置容器对应的页面标志即可。主要概念Native层概念Container:Native容器,平台Controller,Activity,ViewControllerContainer Manager:容器的管理者Adaptor:Flutter是适配层Messaging:基于Channel的消息通信Dart层概念Container:Flutter用来容纳Widget的容器,具体实现为Navigator的派生类-Container Manager:Flutter容器的管理,提供show,remove等ApiCoordinator: 协调器,接受Messaging消息,负责调用Container Manager的状态管理。Messaging:基于Channel的消息通信关于页面的理解在Native和Flutter表示页面的对象和概念是不一致的。在Native,我们对于页面的概念一般是ViewController,Activity。而对于Flutter我们对于页面的概念是Widget。我们希望可统一页面的概念,或者说弱化抽象掉Flutter本身的Widget对应的页面概念。换句话说,当一个Native的页面容器存在的时候,FlutteBoost保证一定会有一个Widget作为容器的内容。所以我们在理解和进行路由操作的时候都应该以Native的容器为准,Flutter Widget依赖于Native页面容器的状态。那么在FlutterBoost的概念里说到页面的时候,我们指的是Native容器和它所附属的Widget。所有页面路由操作,打开或者关闭页面,实际上都是对Native页面容器的直接操作。无论路由请求来自何方,最终都会转发给Native去实现路由操作。这也是接入FlutterBoost的时候需要实现Platform协议的原因。另一方面,我们无法控制业务代码通过Flutter本身的Navigator去push新的Widget。对于业务不通过FlutterBoost而直接使用Navigator操作Widget的情况,包括Dialog这种非全屏Widget,我们建议是业务自己负责管理其状态。这种类型Widget不属于FlutterBoost所定义的页面概念。理解这里的页面概念,对于理解和使用FlutterBoost至关重要。与老方案主要差别前面我们提到老方案在Dart层维护单个Navigator栈结构用于Widget的切换。而新的方案则是在Dart侧引入了Container的概念,不再用栈的结构去维护现有的页面,而是通过扁平化key-value映射的形式去维护当前所有的页面,每个页面拥有一个唯一的id。这种结构很自然的支持了页面的查找和切换,不再受制于栈顶操作的问题,之前的一些由于pop导致的问题迎刃而解。同时也不再需要依赖修改Flutter源码的形式去进行实现,除去了实现的侵入性。那这是如何做到的呢?多Navigator的实现Flutter在底层提供了让你自定义Navigator的接口,我们自己实现了一个管理多个Navigator的对象。当前最多只会有一个可见的Flutter Navigator,这个Navigator所包含的页面也就是我们当前可见容器所对应的页面。Native容器与Flutter容器(Navigator)是一一对应的,生命周期也是同步的。当一个Native容器被创建的时候,Flutter的一个容器也被创建,它们通过相同的id关联起来。当Native的容器被销毁的时候,Flutter的容器也被销毁。Flutter容器的状态是跟随Native容器,这也就是我们说的Native驱动。由Manager统一管理切换当前在屏幕上展示的容器。我们用一个简单的例子描述一个新页面创建的过程:创建Native容器(iOS ViewController,Android Activity or Fragment)。Native容器通过消息机制通知Flutter Coordinator新的容器被创建。Flutter Container Manager进而得到通知,负责创建出对应的Flutter容器,并且在其中装载对应的Widget页面。当Native容器展示到屏幕上时,容器发消息给Flutter Coordinator通知要展示页面的id.Flutter Container Manager找到对应id的Flutter Container并将其设置为前台可见容器。这就是一个新页面创建的主要逻辑,销毁和进入后台等操作也类似有Native容器事件去进行驱动。总结目前FlutterBoost已经在生产环境支撑着在闲鱼客户端中所有的基于Flutter开发业务,为更加负复杂的混合场景提供了支持。同时也解决了一些历史遗留问题。我们在项目启动之初就希望FlutterBoost能够解决Native App混合模式接入Flutter这个通用问题。所以我们把它做成了一个可复用的Flutter插件,希望吸引更多感兴趣的朋友参与到Flutter社区的建设。我们的方案可能不是最好的,这个方案距离完美还有很大的距离,我们希望通过多分享交流以推动Flutter技术社区的发展与建设。我们更希望看到社区能够涌现出更加优秀的组件和方案。在有限篇幅中,我们分享了闲鱼在Flutter混合技术方案中积累的经验和代码。欢迎兴趣的同学能够积极与我们一起交流学习。扩展补充性能相关在两个Flutter页面进行切换的时候,因为我们只有一个Flutter View所以需要对上一个页面进行截图保存,如果Flutter页面多截图会占用大量内存。这里我们采用文件内存二级缓存策略,在内存中最多只保存2-3个截图,其余的写入文件按需加载。这样我们可以在保证用户体验的同时在内存方面也保持一个较为稳定的水平。页面渲染性能方面,Flutter的AOT优势展露无遗。在页面快速切换的时候,Flutter能够很灵敏的相应页面的切换,在逻辑上创造出一种Flutter多个页面的感觉。Release 1.0支持项目开始的时候我们基于闲鱼目前使用的Flutter版本进行开发,而后进行了Release 1.0兼容升级测试目前没有发现问题。接入只要是集成了Flutter的项目都可以用官方依赖的方式非常方便的以插件形式引入FlutterBoost,只需要对工程进行少量代码接入即可完成接入。详细接入文档,请参阅GitHub主页官方项目文档。现已开源目前,新一代混合栈已经在闲鱼全面应用。我们非常乐意将沉淀的技术回馈给社区。欢迎大家一起贡献,一起交流,携手共建Flutter社区。项目开源地址:https://github.com/alibaba/flutter_boost本文作者:闲鱼技术-福居阅读原文本文为云栖社区原创内容,未经允许不得转载。

April 17, 2019 · 1 min · jiezi

燃烧我的卡路里 ---- Flutter瘦内存瘦包之图片组件

背景在电商类APP里,图片到现在为止仍然是最重要的信息承载媒介,不得不说逛淘宝的过程,其实就是一个看图片的过程。而商品详情页中的图片,通常是页面中内存占用最多的内容,占用了整个页面内存的超过 50%。闲鱼在Flutter化的过程中,选择了商品详情页作为第一个落地的场景。通过多版本的迭代完善,基于Flutter的详情页已经在闲鱼稳定运行。然而正因为详情页的图片量大,导致Flutter里图片相关的问题一直挥之不去。1:内存问题 — 连续push flutter界面内存累积2:安装包问题 — 过渡时期两份重复资源文件。3:寻址缓存问题 — 原有的寻址缓存策略无法复用。4:图片复用问题 — Native和Flutter重复下载相同图片。解决方案—-FXTexImage_V1为了解决这些问题,我们尝试着寻找一种新的思路,一种能够将flutter与native串联起来的思路。而之前做视频播放器的方案给了我们启发。熟悉Flutter的同学应该都知道,Flutter的视频组件是基于一个Flutter提供的一个叫“外接纹理”的技术实现的,关于flutter外接纹理,本人另外有一篇文章有更详细的论述,这里不再赘述。https://mp.weixin.qq.com/s/KkCsBvnRayvpXdI35J3fnw我们将每一张图片假想成一个:静态的视频。图片的内容由一个external_texture来负责显示,而这个external_texture则由native端提供具体的渲染数据。通过这种方案,我们便可以通过external_texture这座桥梁,将flutter作为native端图片的一个最终展示场所。而所有的下载、缓存、裁剪等逻辑都可以复用原来的native图片库。基于这个基本框架,我们形成了我们第一版本的图片渲染组件:FXTexImage—-V1。这个组件很好的解决了Flutter引入的安装包、图片缓存、图片复用等问题。但是图片最大的问题:内存问题,并没有得到解决。内存优化—-FXTexImage_V2为了用户体验,通常会有连续push若干个界面的场景(比如闲鱼的详情页,点击底部的推荐列表,可以一直往下push新的详情页),这种场景下,每一个界面都有大量的图片展示。所以在引入flutter以后,闲鱼在iPhone 6P等机型上通常只能push10个左右详情页就挂了。在考虑到在显示过程中,真正用户可见的页面,其实只有当前栈顶的两个页面,基于这个特征我们就做了优化逻辑:1:在push详情页过程中,我们只保留了当前展示页和当前页的前一页的图片资源,而之前的资源全部都做了释放(只是图片资源的释放,整个页面还有页面中的其他元素还是做了保留)。2:为了做到用户无感知,我们在pop过程中,会预先去加载当前界面下一个界面的图片资源。通过这种方式,理论上我们可以释放掉不可见的资源,从而保证在持续Push界面过程中内存缓慢增长,但是实践过程中发现内存仍然持续增长。经过排查,我们发现flutter 1.0版本以及0.8.2版本里,SurfaceTextureRegistry提供了release方法,这里将会把创建的SurfaceTexture进行释放。然而测试过程中发现,单单对SurfaceTexture释放,并没有完全释放内存,当反复创建对象时仍然会闪退。为此,我们在AndroidExternalTextureGL的析构函数中增加了纹理的释放glDeleteTextures逻辑。然而,AndroidExternalTextureGL的析构是在flutter的GPU线程调用的,而external_texture的release方法通常是在主线程,也就是PlatForm线程调用的。不同线程调用的问题就是会导致一个诡异的问题:推测是不同线程释放的逻辑影响了GL环境,导致文字渲染出了问题。所以,为了解决该问题,我们删除了SurfaceTextureRegistry的release方法里面SufaceTexture的释放逻辑,并且将SurfaceTexture的释放,放到AndroidExternalTextureGL析构阶段,通过Jni调用java方法实现资源释放。AndroidExternalTextureGL::~AndroidExternalTextureGL(){ if (state_ == AttachmentState::attached) { Detach(); if (texture_name_ != 0) { glDeleteTextures(1, &texture_name_); texture_name_ = 0; } } Release(); state_ = AttachmentState::detached;}void AndroidExternalTextureGL::Release() { JNIEnv* env = fml::jni::AttachCurrentThread(); fml::jni::ScopedJavaLocalRef<jobject> surfaceTexture = surface_texture_.get(env); if (!surfaceTexture.is_null()) { SurfaceTextureRelease(env, surfaceTexture.obj()); }}void SurfaceTextureRelease(JNIEnv* env, jobject obj) { env->CallVoidMethod(obj, g_release_method); FML_CHECK(CheckException(env));} g_release_method = env->GetMethodID(g_surface_texture_class->obj(), “release”, “()V”);CPU优化—-FXTexImage_V3通过外界纹理渲染图片+不可见页面资源释放,我们解决了上述提出的一系列问题,但是又引入了新的问题:CPU偏高,滑动帧率偏低。通过测试,在详情页滑动过程中,IOS和Android的CPU都比Flutter原生组件高10%以上,这个显然无法应用。经过排查,发现CPU高的原因是:IOS端: iOS的IOSExternalTextureGL模型是一个拉数据的模型,native端register一个CVPixelBuffer的生产者,当需要绘制时,都会调用一次这个生产者的copyPixelbuffer方法去拉一次数据。然后将拉到的CVPixelBuffer对象转换成GPU Texture。这里每一次转换都换造成CPU较大开销。并且这种拉数据的机制就要求这个生产者的必须一直保留着这个CVPixelBuffer对象(否则界面重刷以后,图片区域就显示白屏)。Android端: android 的数据存储在SurfaceTexture中。每一次external_texture layer需要绘制时候都会从SurfaceTexture中去update 数据到纹理中,由于SurfaceTexture使用基于EGLImage共享内存,所以虽然没有双份内存的问题,但是每一次update 都会带来较大的CPU开销。在之前外接纹理的文章中,我们提出了一种新的基于共享上下文的外接纹理方案。并在我们视频的拍摄和编辑中得到了很好的应用。该方案当初提出来,是为了解决视频数据从CPU -> GPU -> CPU -> GPU 输送的问题而提出来的。但是在图片这个场景下, 新的外接纹理方案下,一张图片在native端加载完成以后,立刻被转换成一个OpenGL的Texture,然后图片的资源马上被释放。当界面刷新时,对于同一张图片的重新渲染,IOSExternalTextureGL不需要再去做将数据(CVPixelBuffer或者SurfaceTexture)转换到Texture的逻辑,而是直接使用之前创建好的Texture。经过这一步优化,我们很好的限制了iOS的CPU和内存,Android的CPU。通过测试对比,V3版本的图片组件,相比于Flutter原生图片组件,在详情页正常滑动操作过程中,平均CPU高出3%左右,虽然仍差于原生组件,单相对是可以接受的。结果内存: 基于新图片组件,我们很好的限制住了连续push 下的内存增长速度,顺利的将iPhone 6P上的详情页push 最大数量从10个增加到了30个以上。在同一线上版本中,我们通过控制ABTest开关,测试新的图片渲染方案和Flutter自带图片组件方案的Abort率,发现新图片组件下闲鱼的Abort率降低20%。安装包: 新组件下,所有的资源组件与原来native资源共用,所有flutter期间新引入的资源出了gif图,全部删除,减少安装包900k+。并且后续可以不用继续新增。寻址策略: 复用native图片组件,基于阿里系自己的图片下载组件,这样可以做到随着集团组件升级版本,兼容版本过程中各种新的寻址方式和图片格式。图片复用:复用native图片组件,当图片地址命中缓存,可直接缓存加载,尺寸不一致时可以预先返回缓存图同时加载大图,这样大大增强详情页大图预览的浏览体验。遗留问题图片组件已经在闲鱼上全量部署,然而还是有一些问题没有得到很好的解决,上文提到过CPU比原生图片组件高3%左右,虽然用户没有感官体验,但是还是有优化空间。还有就是Flutter针对ExternalTexture的纹理渲染时没有开启抗锯齿,导致小图在大区域渲染时比原生组件效果要差。这里还需要继续排查原因。最后,FXTexImage组件还在持续优化中,当解决上述遗留问题以后便会在Github上开源。本文作者:闲鱼技术-炉军阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 8, 2019 · 1 min · jiezi

打通前后端逻辑,客户端Flutter代码一天上线

一、前沿 随着闲鱼的业务快速增长,运营类的需求也越来越多,其中不乏有很多界面修改或运营坑位的需求。闲鱼的版本现在是每2周一个版本,如何快速迭代产品,跳过窗口期来满足这些需求?另外,闲鱼客户端的包体也变的很大,企业包的大小,iOS已经到了94.3M,Android也到了53.5M。Android的包体大小,相比2016年,已经增长了近1倍,怎么能将包体大小降下来?首先想到的是如何动态化的解决此类问题。 对于原生的能力的动态化,Android平台各公司都有很完善的动态化方案,甚至Google还提供了Android App Bundles让开发者们更好地支持动态化。由于Apple官方担忧动态化的风险,因此并不太支持动态化。因此动态化能力就会考虑跟Web结合,从一开始基于 WebView 的 Hybrid 方案 PhoneGap、Titanium,到现在与原生相结合的 React Native 、Weex。 但Native和JavaScript Context之间的通讯,频繁的交互就成了程序的性能瓶颈。于此同时随着闲鱼Flutter技术的推广,已经有10多个页面用Flutter实现,上面提到的几种方式都不适合Flutter场景,如何解决这个问题Flutter的动态化的问题?二、动态方案我们最初调研了Google的动态化方案CodePush。2.1 CodePush CodePush是谷歌官方推出的动态化方案,目前只有在Android上面实现了。Dart VM在执行的时候,加载isolate_snapshot_data 和isolate_snapshot_instr 2个文件,通过动态更改这些文件,就达到动态更新的目的。官方的Flutter源码当中,已经有相关的提交来做动态更新的内容,具体内容可以参考 ResourceExtractor.java。 根据官方给出的Guide,我们这边也做了相关的测试,patch的包体大小会很大(939kb)。为了降低包体大小,还可以通过增量的修改snapshot文件的方式来更新。通过bsdiff生成的snapshot的差异文件,2个文件分别可以缩小到48kb和870kb。 目前看来,CodePush还不能做到很好的工程化。而且如何管理patch文件,需要制定baseline和patch文件的规则。2.2 动态模板 动态模板,就是通过定义一套DSL,在端侧解析动态的创建View来实现动态化,比如LuaViewSDK、Tangram-iOS和Tangram-Android。这些方案都是创建的Native的View,如果想在Flutter里面实现,需要创建Texture来桥接;Native端渲染完成之后,再将纹理贴在Flutter的容器里面,实现成本很高,性能也有待商榷,不适合闲鱼的场景。 所以我们提出了闲鱼自己的Flutter动态化方案,前面已经有同事介绍过方案的原理:《做了2个多月的设计和编码,我梳理了Flutter动态化的方案对比及最佳实现》,下面看下具体的实现细节。三、模板编译自定义一套DSL,维护成本较高,怎么能不自定义DSL来实现模板下发?闲鱼的方案就是直接将Dart文件转化成模板,这样模板文件也可以快速沉淀到端侧。3.1 模板规范 先来看下一个完整的模板文件,以新版我的页面为例,这个是一个列表结构,每个区块都是一个独立的Widget,现在我们期望将“卖在闲鱼”这个区块动态渲染,对这个区块拆分之后,需要3个子控件:头部、菜单栏、提示栏;因为这3部分界面有些逻辑处理,所以先把他们的逻辑内置。内置的子控件分别是MenuTitleWidget、MenuItemWidget和HintItemWidget,编写的模板如下:@overrideWidget build(BuildContext context) { return new Container( child: new Column( children: <Widget>[ new MenuTitleWidget(data), // 头部 new Column( // 菜单栏 children: <Widget>[ new Row( children: <Widget>[ new MenuItemWidget(data.menus[0]), new MenuItemWidget(data.menus[1]), new MenuItemWidget(data.menus[2]), ], ) ], ), new Container( // 提示栏 child: new HintItemWidget(data.hints[0])), ], ), );}中间省略了样式描述,可以看到写模板文件就跟普通的widget写法一样,但是有几点要注意:每个Widget都需要用new或const来修饰数据访问以data开头,数组形式以[]访问,字典形式以.访问 模板写好之后,就要考虑怎么在端上渲染,早期版本是直接在端侧解析文件,但是考虑到性能和稳定性,还是放在前期先编译好,然后下发到端侧。3.2 编译流程 编译模板就要用到Dart的Analyzer库,通过parseCompilationUnit函数直接将Dart源码解析成为以CompilationUnit为Root节点的AST树中,它包含了Dart源文件的语法和语义信息。接下来的目标就是将CompilationUnit转换成为一个JSON格式。 上面的模板解析出来build函数孩子节点是ReturnStatementImpl,它又包含了一个子节点InstanceCreationExpressionImpl,对应模板里面的new Container(…),它的孩子节点中,我们最关心的就是ConstructorNameImpl和ArgumentListImpl节点。ConstructorNameImpl标识创建节点的名称,ArgumentListImpl标识创建参数,参数包含了参数列表和变量参数。定义如下结构体,来存储这些信息:class ConstructorNode { // 创建节点的名称 String constructorName; // 参数列表 List<dynamic> argumentsList = <dynamic>[]; // 变量参数 Map<String, dynamic> arguments = <String, dynamic>{};}递归遍历整棵树,就可以得到一个ConstructorNode树,以下代码是解析单个Node的参数:ArgumentList argumentList = astNode;for (Expression exp in argumentList.arguments) { if (exp is NamedExpression) { NamedExpression namedExp = exp; final String name = ASTUtils.getNodeString(namedExp.name); if (name == ‘children’) { continue; } /// 是函数 if (namedExp.expression is FunctionExpression) { currentNode.arguments[name] = FunctionExpressionParser.parse(namedExp.expression); } else { /// 不是函数 currentNode.arguments[name] = ASTUtils.getNodeString(namedExp.expression); } } else if (exp is PropertyAccess) { PropertyAccess propertyAccess = exp; final String name = ASTUtils.getNodeString(propertyAccess); currentNode.argumentsList.add(name); } else if (exp is StringInterpolation) { StringInterpolation stringInterpolation = exp; final String name = ASTUtils.getNodeString(stringInterpolation); currentNode.argumentsList.add(name); } else if (exp is IntegerLiteral) { final IntegerLiteral integerLiteral = exp; currentNode.argumentsList.add(integerLiteral.value); } else { final String name = ASTUtils.getNodeString(exp); currentNode.argumentsList.add(name); }}端侧拿到这个ConstructorNode节点树之后,就可以根据Widget的名称和参数,来生成一棵Widget树。四、渲染引擎端侧拿到编译好的模板JSON后,就是解析模板并创建Widget。先看下,整个工程的框架和工作流:工作流程:开发人员编写dart文件,编译上传到CDN端侧拿到模板列表,并在端侧存库业务方直接下发对应的模板id和模板数据Flutter侧再通过桥接获取到模板,并创建Widget树对于Native测,主要负责模板的管理,通过桥接输出到Flutter侧。4.1 模板获取模板获取分为2部分,Native部分和Flutter部分;Native主要负责模板的管理,包括下载、降级、缓存等。程序启动的时候,会先获取模板列表,业务方需要自己实现,Native层获取到模板列表会先存储在本地数据库中。Flutter侧业务代码用到模板的时候,再通过桥接获取模板信息,就是我们前面提到的JSON格式的信息,Flutter也会有缓存,已减少Flutter和Native的交互。4.2 Widget创建Flutter侧当拿到JSON格式的,先解析出ConstructorNode树,然后递归创建Widget。创建每个Widget的过程,就是解析节点中的argumentsList和arguments 并做数据绑定。例如,创建HintItemWidget需要传入提示的数据内容,new HintItemWidget(data.hints[0]),在解析argumentsList时,会通过key-path的方式从原始数据中解析出特定的值。解析出来的值都会存储在WidgetCreateParam里面,当递归遍历每个创建节点,每个widget都可以从WidgetCreateParam里面解析出需要的参数。/// 构建widget用的参数class WidgetCreateParam { String constructorName; /// 构建的名称 dynamic context; /// 构建的上下文 Map<String, dynamic> arguments = <String, dynamic>{}; /// 字典参数 List<dynamic> argumentsList = <dynamic>[]; /// 列表参数 dynamic data; /// 原始数据} 通过以上的逻辑,就可以将ConstructorNode树转换为一棵Widget树,再交给Flutter Framework去渲染。至此,我们已经能将模板解析出来,并渲染到界面上,交互事件应该怎么处理?4.3 事件处理在写交互的时候,一般都会通过GestureDector、InkWell等来处理点击事件。交互事件怎么做动态化? 以InkWell组件为例,定义它的onTap函数为openURL(data.hints[0].href, data.hints[0].params)。在创建InkWell时,会以OpenURL作为事件ID,查找对应的处理函数,当用户点击的时候,会解析出对应的参数列表并传递过去,代码如下:…final List<dynamic> tList = <dynamic>[];// 解析出参数列表exp.argumentsList.forEach((dynamic arg) { if (arg is String) { final dynamic value = valueFromPath(arg, param.data); if (value != null) { tList.add(value); } else { tList.add(arg); } } else { tList.add(arg); }});// 找到对应的处理函数final dynamic handler = TeslaEventManager.sharedInstance().eventHandler(exp.actionName);if (handler != null) { handler(tList);}…五、 效果新版我的页面添加了动态化渲染能力之后,如果有需求新添加一种组件类型,就可以直接编译发布模板,服务端下发新的数据内容,就可以渲染出来了;动态化能力有了,大家会关心渲染性能怎么样。5.1 帧率在加了动态加载逻辑之后,已经开放了2个动态卡片,下图是新版本我的页面近半个月的的帧率数据:从上图可以看到,帧率并没有降低,基本保持在55-60帧左右,后续可以多添加动态的卡片,观察下效果。注:因为我的页面会有本地的一些业务判断,从其他页面回到我的tab,都会刷新界面,所以帧率会有损耗。 从实现上分析,因为每个卡片,都需要遍历ConstructorNode树来创建,而且每个构建都需要解析出里面的参数,这块可以做一些优化,比如缓存相同的Widget,只需要映射出数据内容并做数据绑定。5.2 失败率现在监控了渲染的逻辑,如果本地没有对应的Widget创建函数,会主动抛Error。监控数据显示,渲染的流程中,还没有异常的情况,后续还需要对桥接层和native层加错误埋点。六、展望 基于Flutter动态模板,之前需要走发版的Flutter需求,都可以来动态化更改。而且以上逻辑都是基于Flutter原生的体系,学习和维护成本都很低,动态的代码也可以快速的沉淀到端侧。 另外,闲鱼正在研究UI2Code的黑科技,不了解的老铁,可以参考闲鱼大神的这篇文章《重磅系列文章!UI2CODE智能生成Flutter代码——整体设计篇》。可以设想下,如果有个需求,需要动态的显示一个组件,UED出了视觉稿,通过UI2Code转换成Dart文件,再通过这个系统转换成动态模板,下发到端侧就可以直接渲染出来,程序员都不需要写代码了,做到自动化运营,看来以后程序员失业也不是没有可能了。 基于Flutter的Widget,还可以拓展更多个性化的组件,比如内置动画组件,就可以动态化下发动画了,更多好玩的东西等待大家来一起探索。参考文献https://github.com/flutter/flutter/issues/14330https://www.dartlang.org/https://mp.weixin.qq.com/s/4s6MaiuW4VoHr_7f0S_vuQhttps://github.com/flutter/engine本文作者:闲鱼技术-景松阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 4, 2019 · 2 min · jiezi

码上用它开始Flutter混合开发——FlutterBoost

开源地址:https://github.com/alibaba/flutter_boost为什么要混合方案具有一定规模的App通常有一套成熟通用的基础库,尤其是阿里系App,一般需要依赖很多体系内的基础库。那么使用Flutter重新从头开发App的成本和风险都较高。所以在Native App进行渐进式迁移是Flutter技术在现有Native App进行应用的稳健型方式。闲鱼在实践中沉淀出一套自己的混合技术方案。在此过程中,我们跟Google Flutter团队进行着密切的沟通,听取了官方的一些建议,同时也针对我们业务具体情况进行方案的选型以及具体的实现。官方提出的混合方案1基本原理Flutter技术链主要由C++实现的Flutter Engine和Dart实现的Framework组成(其配套的编译和构建工具我们这里不参与讨论)。Flutter Engine负责线程管理,Dart VM状态管理和Dart代码加载等工作。而Dart代码所实现的Framework则是业务接触到的主要API,诸如Widget等概念就是在Dart层面Framework内容。一个进程里面最多只会初始化一个Dart VM。然而一个进程可以有多个Flutter Engine,多个Engine实例共享同一个Dart VM。我们来看具体实现,在iOS上面每初始化一个FlutterViewController就会有一个引擎随之初始化,也就意味着会有新的线程(理论上线程可以复用)去跑Dart代码。Android类似的Activity也会有类似的效果。如果你启动多个引擎实例,注意此时Dart VM依然是共享的,只是不同Engine实例加载的代码跑在各自独立的Isolate。2官方建议引擎深度共享在混合方案方面,我们跟Google讨论了可能的一些方案。Flutter官方给出的建议是从长期来看,我们应该支持在同一个引擎支持多窗口绘制的能力,至少在逻辑上做到FlutterViewController是共享同一个引擎的资源的。换句话说,我们希望所有绘制窗口共享同一个主Isolate。但官方给出的长期建议目前来说没有很好的支持。多引擎模式我们在混合方案中解决的主要问题是如何去处理交替出现的Flutter和Native页面。Google工程师给出了一个Keep It Simple的方案:对于连续的Flutter页面(Widget)只需要在当前FlutterViewController打开即可,对于间隔的Flutter页面我们初始化新的引擎。例如,我们进行下面一组导航操作:我们只需要在Flutter Page1和Flutter Page3创建不同的Flutter实例即可。这个方案的好处就是简单易懂,逻辑清晰,但是也有潜在的问题。如果一个Native页面一个Flutter页面一直交替进行的话,Flutter Engine的数量会线性增加,而Flutter Engine本身是一个比较重的对象。多引擎模式的问题冗余的资源问题.多引擎模式下每个引擎之间的Isolate是相互独立的。在逻辑上这并没有什么坏处,但是引擎底层其实是维护了图片缓存等比较消耗内存的对象。想象一下,每个引擎都维护自己一份图片缓存,内存压力将会非常大。插件注册的问题。插件依赖Messenger去传递消息,而目前Messenger是由FlutterViewController(Activity)去实现的。如果你有多个FlutterViewController,插件的注册和通信将会变得混乱难以维护,消息的传递的源头和目标也变得不可控。Flutter Widget和Native的页面差异化问题。Flutter的页面是Widget,Native的页面是VC。逻辑上来说我们希望消除Flutter页面与Naitve页面的差异,否则在进行页面埋点和其它一些统一操作的时候都会遇到额外的复杂度。增加页面之间通信的复杂度。如果所有Dart代码都运行在同一个引擎实例,它们共享一个Isolate,可以用统一的编程框架进行Widget之间的通信,多引擎实例也让这件事情更加复杂。因此,综合多方面考虑,我们没有采用多引擎混合方案。现状及思考前面我们提到多引擎存在一些实际问题,所以闲鱼目前采用的混合方案是共享同一个引擎的方案。这个方案基于这样一个事实:任何时候我们最多只能看到一个页面,当然有些特定的场景你可以看到多个ViewController,但是这些特殊场景我们这里不讨论。我们可以这样简单去理解这个方案:我们把共享的Flutter View当成一个画布,然后用一个Native的容器作为逻辑的页面。每次在打开一个容器的时候我们通过通信机制通知Flutter View绘制成当前的逻辑页面,然后将Flutter View放到当前容器里面。这个方案无法支持同时存在多个平级逻辑页面的情况,因为你在页面切换的时候必须从栈顶去操作,无法再保持状态的同时进行平级切换。举个例子:有两个页面A,B,当前B在栈顶。切换到A需要把B从栈顶Pop出去,此时B的状态丢失,如果想切回B,我们只能重新打开B之前页面的状态无法维持住。如在pop的过程当中,可能会把Flutter 官方的Dialog进行误杀。而且基于栈的操作我们依赖对Flutter框架的一个属性修改,这让这个方案具有了侵入性的特点。具体细节,大家可以参考老方案开源项目地址:https://github.com/alibaba-flutter/hybridstackmanager新一代混合技术方案 FlutterBoost1重构计划在闲鱼推进Flutter化过程当中,更加复杂的页面场景逐渐暴露了老方案的局限性和一些问题。所以我们启动了代号FlutterBoost(向C++ Boost库致敬)的新混合技术方案。这次新的混合方案我们的主要目标有:可复用通用型混合方案支持更加复杂的混合模式,比如支持主页Tab这种情况无侵入性方案:不再依赖修改Flutter的方案支持通用页面生命周期统一明确的设计概念跟老方案类似,新的方案还是采用共享引擎的模式实现。主要思路是由Native容器Container通过消息驱动Flutter页面容器Container,从而达到Native Container与Flutter Container的同步目的。我们希望做到Flutter渲染的内容是由Naitve容器去驱动的。简单的理解,我们想做到把Flutter容器做成浏览器的感觉。填写一个页面地址,然后由容器去管理页面的绘制。在Native侧我们只需要关心如果初始化容器,然后设置容器对应的页面标志即可。2主要概念3Native层概念Container:Native容器,平台Controller,Activity,ViewControllerContainer Manager:容器的管理者Adaptor:Flutter是适配层Messaging:基于Channel的消息通信4Dart层概念Container:Flutter用来容纳Widget的容器,具体实现为Navigator的派生类-Container Manager:Flutter容器的管理,提供show,remove等ApiCoordinator: 协调器,接受Messaging消息,负责调用Container Manager的状态管理。Messaging:基于Channel的消息通信5关于页面的理解在Native和Flutter表示页面的对象和概念是不一致的。在Native,我们对于页面的概念一般是ViewController,Activity。而对于Flutter我们对于页面的概念是Widget。我们希望可统一页面的概念,或者说弱化抽象掉Flutter本身的Widget对应的页面概念。换句话说,当一个Native的页面容器存在的时候,FlutteBoost保证一定会有一个Widget作为容器的内容。所以我们在理解和进行路由操作的时候都应该以Native的容器为准,Flutter Widget依赖于Native页面容器的状态。那么在FlutterBoost的概念里说到页面的时候,我们指的是Native容器和它所附属的Widget。所有页面路由操作,打开或者关闭页面,实际上都是对Native页面容器的直接操作。无论路由请求来自何方,最终都会转发给Native去实现路由操作。这也是接入FlutterBoost的时候需要实现Platform协议的原因。另一方面,我们无法控制业务代码通过Flutter本身的Navigator去push新的Widget。对于业务不通过FlutterBoost而直接使用Navigator操作Widget的情况,包括Dialog这种非全屏Widget,我们建议是业务自己负责管理其状态。这种类型Widget不属于FlutterBoost所定义的页面概念。理解这里的页面概念,对于理解和使用FlutterBoost至关重要。6与老方案主要差别前面我们提到老方案在Dart层维护单个Navigator栈结构用于Widget的切换。而新的方案则是在Dart侧引入了Container的概念,不再用栈的结构去维护现有的页面,而是通过扁平化key-value映射的形式去维护当前所有的页面,每个页面拥有一个唯一的id。这种结构很自然的支持了页面的查找和切换,不再受制于栈顶操作的问题,之前的一些由于pop导致的问题迎刃而解。也不需要依赖修改Flutter源码的形式去进行页面栈操作,去掉了实现的侵入性。实际上我们引入的Container就是Navigator的,也就是说一个Native的容器对应了一个Navigator。那这是如何做到的呢?7多Navigator的实现Flutter在底层提供了让你自定义Navigator的接口,我们自己实现了一个管理多个Navigator的对象。当前最多只会有一个可见的Flutter Navigator,这个Navigator所包含的页面也就是我们当前可见容器所对应的页面。Native容器与Flutter容器(Navigator)是一一对应的,生命周期也是同步的。当一个Native容器被创建的时候,Flutter的一个容器也被创建,它们通过相同的id关联起来。当Native的容器被销毁的时候,Flutter的容器也被销毁。Flutter容器的状态是跟随Native容器,这也就是我们说的Native驱动。由Manager统一管理切换当前在屏幕上展示的容器。我们用一个简单的例子描述一个新页面创建的过程:创建Native容器(iOS ViewController,Android Activity or Fragment)。Native容器通过消息机制通知Flutter Coordinator新的容器被创建。Flutter Container Manager进而得到通知,负责创建出对应的Flutter容器,并且在其中装载对应的Widget页面。当Native容器展示到屏幕上时,容器发消息给Flutter Coordinator通知要展示页面的id.Flutter Container Manager找到对应id的Flutter Container并将其设置为前台可见容器。这就是一个新页面创建的主要逻辑,销毁和进入后台等操作也类似有Native容器事件去进行驱动。总结目前FlutterBoost已经在生产环境支撑着在闲鱼客户端中所有的基于Flutter开发业务,为更加负复杂的混合场景提供了支持,稳定为亿级用户提供服务。我们在项目启动之初就希望FlutterBoost能够解决Native App混合模式接入Flutter这个通用问题。所以我们把它做成了一个可复用的Flutter插件,希望吸引更多感兴趣的朋友参与到Flutter社区的建设。在有限篇幅中,我们分享了闲鱼在Flutter混合技术方案中积累的经验和代码。欢迎兴趣的同学能够积极与我们一起交流学习。扩展补充1性能相关在两个Flutter页面进行切换的时候,因为我们只有一个Flutter View所以需要对上一个页面进行截图保存,如果Flutter页面多截图会占用大量内存。这里我们采用文件内存二级缓存策略,在内存中最多只保存2-3个截图,其余的写入文件按需加载。这样我们可以在保证用户体验的同时在内存方面也保持一个较为稳定的水平。页面渲染性能方面,Flutter的AOT优势展露无遗。在页面快速切换的时候,Flutter能够很灵敏的相应页面的切换,在逻辑上创造出一种Flutter多个页面的感觉。2Release1.0的支持项目开始的时候我们基于闲鱼目前使用的Flutter版本进行开发,而后进行了Release 1.0兼容升级测试目前没有发现问题。3接入只要是集成了Flutter的项目都可以用官方依赖的方式非常方便的以插件形式引入FlutterBoost,只需要对工程进行少量代码接入即可完成接入。详细接入文档,请参阅GitHub主页官方项目文档。4现已开源目前,新一代混合栈已经在闲鱼全面应用。我们非常乐意将沉淀的技术回馈给社区。欢迎大家一起贡献,一起交流,携手共建Flutter社区。项目开源地址:https://github.com/alibaba/flutter_boost本文作者:福居阅读原文本文来自云栖社区合作伙伴“闲鱼技术”,如需转载请联系原作者。

March 18, 2019 · 1 min · jiezi

程序员如何让自己 Be Cloud Native - 配置篇

前言这是《程序员如何让自己 Be Cloud Native》系列文章的第二篇,从第一篇的反馈来看,有些同学反馈十二要素太形式主义,不建议盲目跟从。作者认为任何理论和技术都需要有自己的观点,这些观点是建立在个体知识体系逐渐锻炼出来的辩别能力之上的。Be Cloud Native这一系列的文章,会基于十二要素为理论基础,加上作者在云计算诞生以来对于架构的演进所观察到的变化去分享自己的一些心得。第一篇:仓库与依赖。「传送门」实例配置这个要素的核心思想就是代码与数据隔离,一开始我们的软件很小很小的时候,我们会可能直接把各种配置、甚至生产环境中的代码直接写在代码中,配置甚至就是代码的一部分?比如以下的这断代码就是这样:public boolean serviceConnectable() { return ping(“edas.console.aliyun.com”, 3); }该方法实现一个本地是否可以连接到远端 server 的功能。如果按照上面的代码进行编写,只能保证在公共云的环境达到想要的效果。该程序如果部署到了一个专有云或者 IDC 的环境中的时候,就需要改代码了。如果改成以下的方式,效果就会截然不同。那时如果程序部署到了一套新的环境中,只需改变edas.server.address 这个配置。@Value(“edas.server.address”)private String remoteAddress;public boolean serviceConnectable() {return ping(remoteAddress, 3);}定义与示例这个例子应该比较贴近大家的日常,我们将上面这个例子往上提升一层,可以做出一个如下的初步定义:应用的行为(Behavior) = 代码(Code) + 输入(Data)。代码是固定的,需要重新编译分发,无法根据环境进行变化的;而输入是活的。上面的例子,就是一个把 Code 中的一部分内容抽离,变成 Data 的过程。做完这个变化之后,应用对变化的适应性就更强了。当然,这些 Data 有些是用户输入的,有些是系统启动时就已经确定的,后者是我们定义的 配置 ,也是我们今天讨论的主题。从这个层面(Data)说起来,配置其实可以包含很多种,以 Java 语言为例,至少分为以下几种:代码中的文件配置: *.properties 文件。机器上的文件配置 *.properties 文件。通过 -D 参数指定的启动参数。环境变量。配置管理系统,简单的有 DB;比较流行的领域产品如 Nacos、ZooKeeper、ectd 等。选择多了,貌似世界就不那么美妙了,因为我们总是会陷入到“用什么”和“为什么”中去。作者的观点是,在用什么之前,先弄清楚需求层面的两点:隔离粒度:每个版本不一样?每台机器不一样?每个进程不一样?安全性:运维人员可见?开发人员可见?还是都不应该可见?仔细讨论上述两点之前,我们举几个关于配置的例子:程序版本号:从代码 Release (build) 开始,基本上就确定了;这一类配置基本上不存在变化的可能,即这一类配置的隔离粒度等同于 Code 。某个前端组件的版本号:前端版本号有一个特点,就是变动很频繁,尤其是在上线的过程中,发布三四个版本是很常见的现象;而且在上线的过程中,一般都会先灰度验证,再进行现网发布。所以这类配置需要具备一个特点就是,可灵活变动与可按照环境(不同机器、不同流量)粒度发布。服务器端口号:服务器的端口号是需要和进程绑定的,尤其在某些微服务场景;同一个服务,如果部署在同一台机器上,必须准确的告知其他服务本服务的确切的地址和端口。数据库元信息配置:这种配置,同一套环境中的相同服务会是一样的,而且其真实的值隐藏的越深越好,其他还有某些 AK/SK、用户名密码之类,每套环境会不一样,同时不同的产品对待这类配置的安全性要求也可能不一样。归纳通过上面例子简单的论述,我们大致可以把相关的配置做如下的归类:配置项所在位置隔离性安全性代码文件控制不同版本的软件行为,等同于代码、基本不会改动所有有代码权限的人员都可见机器上的文件机器环境级别的隔离可根据文件的系统权限灵活设置可见性启动参数进程级别隔离能进入系统便可见环境变量可根据容器、系统、用户、进程进行非常灵活的搭配可设置到系统用户级别配置管理系统程序员可自由编程实现,一般是服务级别的隔离性取决于不同产品的实现,有的方案可以做到安全性最好这里作者想额外强调的是安全性这一个点,尤其某些金融场景。原生的配置方式,如果不做代码的改动的话,都无法做到很高的安全性,但是在一些分布式产品中,尤其是一些云产品内部,就可以做到很安全,具体可以参考下图:以 Nacos 的云上实现 ACM 为例,对上图进行一个简单的阐述。一般的程序读取配置方式如左图,当执行启动脚本后,应用程序从脚本中设置的环境变量、文件或启动参数中获取配置。这些方式可以满足大部分的场景,但是如果你的应用是一个分布式的大集群,这个时候如果想改一个配置是不可能在机器上配置的,然后一台台的区修改,此时我们需要一个支持大集群的分布式配置服务来支持。开源的配置中心有很多,如 ZooKeeper、etcd、Nacos 等。但是有一种场景,一般意义上的配置中心也是满足不了的,那就是诸如数据库密码这一类安全性要求很高的配置。这类在云上会有一些很好的实现,以上图右边为例解释一下在云上是如何做到的:当用户往配置服务中写入一个配置时,会先使用加密服务进行加密,图中的 A。如果有客户端需要,将相应的加密数据推往对应的客户端,图中的 B。客户端收到数据,会根据机器上的_云角色_,请求云加密服务进行解密,图中的 C。从上面这个描述,我们就能很感受到整个过程,利用云生态的能力,可以做得很优雅、很安全,而且也没有额外的代码侵入。总结写到这里,我需要点一下题,要做到 “Be Cloud Native” ,配置是必不可少的一个环节。能让我们的世界变得稍微美好点的方式之一,就是把每个硬编码的字符,变成一个个可运维、安全的配置。同时在云上,我们会看到有不一样的、更加优雅、更安全、成本更低的解决方案。配置的安全无小事,一时图简单省事,可能就会造成生产级别的敏感信息、甚至 DB 的泄露。Be Cloud Native 的另外一层意思,正是尽可能多的利用云厂商提供的原生技术能力,来构建一个更安全、优雅、可扩展、高可用的应用架构。本文作者:中间件小哥阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

March 4, 2019 · 1 min · jiezi

能用机器完成的,千万别堆工作量|持续集成中的性能自动化测试

1.背景当前闲鱼在精益开发模式下,整个技术团队面临了诸多的能力落地和挑战,尤其是效能方面的2-1-1的目标(2周需求交付周期,1周需求开发周期,1小时达到发布标准),具体可见 闲鱼工程师是如何构建持续集成流水线,让研发效率翻倍的 ,在这个大目标下,就必须把每个环节都做到极致。自动化的建设是决定CI成败的关键能力,今天分享一下闲鱼Android客户端性能自动化环节的实践。2.面临的问题2.1 主要是两个方面的问题工具缺失:目前淘宝系,对于线上性能水位的监控有一套完善的体系,但是针对新功能的性能测试,每个业务团队都有对应的性能专项小组,产出的工具都是根据自己业务特点的定制开发的,闲鱼客户端目前使用Flutter做为客户端主开发语言,对于Flutter性能数据的获取及UI自动化测试支撑工具目前是缺失的,同时业界对Flutter自动化和性能相关的实践几乎没有;测试工作量翻N倍(N=一个版本周期内的分支数):原先的开发模式是功能测试集成测试一起进行的,所以性能测试只需要针对集成后的包进行测试即可,到现在转变为泳道的开发模式,一个版本内会一般包含十几个左右的泳道分支甚至更多,我们必须确保每个泳道的分支的性能是达标的,如果有性能问题需要第一时间反馈出来,如果遗留到集成阶段,问题的排查(十几个分支中筛查),问题的解决将会耗费大量的时间,效率很难得到大的提升;2.2 问题思考体系化解决,要让每个泳道分支都得到有效测试覆盖,测试件能够自动化执行,持续反馈结果3. 解决方案综合上述问题,梳理如下解决方案:针对Flutter性能数据的获取(比如,Flutter有自己的SurfaceView,原有Native计算FPS的方式无法直接使用)针对Flutter UI自动化的实现(Flutter/Native UI混合栈的处理)性能自动化脚本 / 性能数据自动采集、上报 融入CI流程性能问题的通知 / 报表展示 / 分析3.1 性能数据[FPS]解析处理 adb shell dumpsys SurfaceFlinger –latency 的数据,详细请见文末参考链接(该方式兼容Flutter及Native的解决方案,已在Android4.x-9.x验证可行),处理SurfaceFlinger核心代码如下:dumpsys SurfaceFlinger –latency-clear#echo “dumpsys SurfaceFlinger…“if [[ $isflutter = 0 ]];then window=dumpsys window windows | grep mCurrent | $bb awk '{print $3}'|$bb tr -d '}' # Get the current window echo $windowfiif [[ $isflutter = 1 ]];then window=dumpsys SurfaceFlinger --list |grep '^SurfaceView'|$bb awk 'NR==1{print $0}'if [ -z “$window” ]; then window=“SurfaceView"fiecho $windowfi$bb usleep $sleep_tdumpsys SurfaceFlinger –latency “$window”|$bb awk -v time=$uptime -v target=$target -v kpi=$KPI ‘{if(NR==1){r=$1/1000000;if(r<0)r=$1/1000;b=0;n=0;w=1}else{if(n>0&&$0==”")O=1;if(NF==3&&$2!=0&&$2!=9223372036854775807){x=($3-$1)/1000000/r;if(b==0){b=$2;n=1;d=0;D=0;if(x<=1)C=r;if(x>1){d+=1;C=int(x)r;if(x%1>0)C+=r};if(x>2)D+=1;m=r;o=0}else{c=($2-b)/1000000;if(c>1000){O=1}else{n+=1;if(c>=r){C+=c;if(c>kpi)o+=1;if(c>=m)m=c;if(x>1)d+=1;if(x>2)D+=1;b=$2}else{C+=r;b=sprintf(”%.0f”,b+r1000000)}}};if(n==1)s=sprintf("%.3f",$2/1000000000)};if(n>0&&O==1){O=0;if(n==1)t=sprintf("%.3f",s+C/1000);else t=sprintf("%.3f",b/1000000000);T=strftime("%F %T",time+t)".“sprintf(”%.0f",(time+t)%11000);f=sprintf("%.2f",n1000/C);m=sprintf("%.0f",m);g=f/target;if(g>1)g=1;h=kpi/m;if(h>1)h=1;e=sprintf("%.2f",g60+h20+(1-o/n)*20);print s",“t”,“T”,“f+0”,“n”,“d”,“D”,“m”,“o”,“e”,“w;n=0;if($0==”"){b=0;w+=1}else{b=$2;n=1;d=0;D=0;if(x<=1)C=r;if(x>1){d+=1;C=int(x)*r;if(x%1>0)C+=r};if(x>2)D+=1;m=r;o=0}}}}’ >>$file[CPU]使用的是top的命令获取(该方式获取性能数据时,数据收集带来的损耗最少)export bb="/data/local/tmp/busybox"$bb top -b -n 1|$bb awk ‘NR==4{print NF-1}’[内存]解析 dumpsys meminfo $package 拿到 Java Heap,Java Heap Average,Java Heap Peak,Native Heap,Native Heap Average,Native Heap Peak,Graphics,Unknown,Pss 数据do_statistics() { ((COUNT+=1)) isExist="$(echo $OUTPUT | grep “Dalvik Heap”)" if [[ ! -n $isExist ]] ; then old_dumpsys=true else old_dumpsys=false fi if [[ $old_dumpsys = true ]] ; then java_heap="$(echo “$OUTPUT” | grep “Dalvik” | $bb awk ‘{print $6}’ | $bb tr -d ‘\r’)" else java_heap="$(echo “$OUTPUT” | grep “Dalvik Heap[^:]” | $bb awk ‘{print $8}’ | $bb tr -d ‘\r’)" fi echo “1."$JAVA_HEAP_TOTAL “2."$java_heap “3."$JAVA_HEAP_TOTAL ((JAVA_HEAP_TOTAL+=java_heap)) ((JAVA_HEAP_AVG=JAVA_HEAP_TOTAL/COUNT)) if [[ $java_heap -gt $JAVA_HEAP_PEAK ]] ; then JAVA_HEAP_PEAK=$java_heap fi if [[ $old_dumpsys = true ]] ; then native_heap="$(echo “$OUTPUT” | grep “Native” | $bb awk ‘{print $6}’ | $bb tr -d ‘\r’)” else native_heap="$(echo “$OUTPUT” | grep “Native Heap[^:]” | $bb awk ‘{print $8}’ | $bb tr -d ‘\r’ | $bb tr -d ‘\n’)” fi ((NATIVE_HEAP_TOTAL+=native_heap)) ((NATIVE_HEAP_AVG=NATIVE_HEAP_TOTAL/COUNT)) if [[ $native_heap -gt $NATIVE_HEAP_PEAK ]] ; then NATIVE_HEAP_PEAK=$native_heap fi g_Str=“Graphics” if [[ $OUTPUT == $g_Str ]] ; then echo “Found Graphics…” Graphics="$(echo “$OUTPUT” | grep “Graphics” | $bb awk ‘{print $2}’ | $bb tr -d ‘\r’)” else echo “Not Found Graphics…” Graphics=0 fi Unknown="$(echo “$OUTPUT” | grep “Unknown” | $bb awk ‘{print $2}’ | $bb tr -d ‘\r’)" total="$(echo “$OUTPUT” | grep “TOTAL”|$bb head -1| $bb awk ‘{print $2}’ | $bb tr -d ‘\r’)"}[流量]通过 dumpsys package packages 解析出当前待测试包来获取流量信息uid="$(dumpsys package packages|$bb grep -E “Package |userId”|$bb awk -v OFS=" " ‘{if($1==“Package”){P=substr($2,2,length($2)-2)}else{if(substr($1,1,6)==“userId”)print P,substr($1,8,length($1)-7)}}’|grep $package|$bb awk ‘{print $2}’)“echo “Net:"$uidinitreceive=$bb awk -v OFS=" " 'NR&gt;1{if($2=="wlan0"){wr[$4]+=$6;wt[$4]+=$8}else{if($2=="rmnet0"){rr[$4]+=$6;rt[$4]+=$8}}}END{for(i in wr){print i,wr[i]/1000,wt[i]/1000,"wifi"};for(i in rr){print i,rr[i]/1000,rt[i]/1000,"data"}}' /proc/net/xt_qtaguid/stats | grep $uid|$bb awk '{print $2}'inittransmit=$bb awk -v OFS=" " 'NR&gt;1{if($2=="wlan0"){wr[$4]+=$6;wt[$4]+=$8}else{if($2=="rmnet0"){rr[$4]+=$6;rt[$4]+=$8}}}END{for(i in wr){print i,wr[i]/1000,wt[i]/1000,"wifi"};for(i in rr){print i,rr[i]/1000,rt[i]/1000,"data"}}' /proc/net/xt_qtaguid/stats | grep $uid|$bb awk '{print $3}'echo “initnetarray”$initreceive”,"$inittransmitgetnet(){ local data_t=date +%Y/%m/%d" "%H:%M:%S netdetail=$bb awk -v OFS=, -v initreceive=$initreceive -v inittransmit=$inittransmit -v datat="$data_t" 'NR&gt;1{if($2=="wlan0"){wr[$4]+=$6;wt[$4]+=$8}else{if($2=="rmnet0"){rr[$4]+=$6;rt[$4]+=$8}}}END{for(i in wr){print datat,i,wr[i]/1000-initreceive,wt[i]/1000-inittransmit,"wifi"};for(i in rr){print datat,i,rr[i]/1000-initreceive,rt[i]/1000-inittransmit,"data"}}' /proc/net/xt_qtaguid/stats | grep $uid echo $netdetail>>$filenet}3.2 性能自动化脚本基于Appium的自动化用例,这个技术业界已经有非常多的实践了,这里我不再累述,如果不了解的同学,可以到Appium官网 http://appium.ioFlutter和Native页面切换使用App内的Schema跳转Flutter页面的文本输入等交互性较强的场景使用基于Flutter框架带的Integration Test来操作An integration testGenerally, an integration test runs on a real device or an OS emulator, such as iOS Simulator or Android Emulator. The app under test is typically isolated from the test driver code to avoid skewing the results.Flutter的UI自动化及Flutter/Native混合页面的处理在测试上的应用后续单独开文章介绍,原理相关可以先参考 千人千面录制回放技术3.3 性能自动化CI流程3.4 性能数据报表FPS相关Framediff: 绘制帧的开始时间和结束时间差FPS: 每秒展示的帧数Frames: 一个刷新周期内所有的帧jank: 一帧开始绘制到结束超过16.67ms 就记一次jank,jank非零代表硬件绘制掉帧,和屏幕硬件性能及相关驱 动性能有关jank2: 一帧开始绘制到结束超过33.34ms 就记一次jank2MFS: 在一个刷新周期内单帧最大耗时(每两行垂直同步的时间差代表两帧绘制的帧间隔)OKT: 在一个刷新周期内,帧耗时超过16.67ms的次数SS: 流畅度,通过FPS,MFS,OKT计算出来,流畅度 = 实际帧率比目标帧率比值60【目标帧率越高越好】 + 目标时间和两帧时间差比值20【两帧时间差越低越好】 + (1-超过16ms次数/帧数)*20【次数越少越好】CPUMemory4. 成果展示4.1 指定泳道分支性能监控泳道分支出现了性能问题再报表上一目了然4.2 性能专项支撑1、Flutter商品详情页重构 14轮测试2、客户端图片统一资源测试 4轮测试5. 总结性能自动化只是整个CI流程中的一个环节,为了极致效率的大目标,闲鱼质量团队还产出了很多支撑工具,CI平台,遍历测试,AI错误识别,用例自动生成等等,后续也会分享给大家。6. 参考https://testerhome.com/topics/2232https://testerhome.com/topics/4775本文作者:闲鱼技术-灯阳阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 22, 2019 · 3 min · jiezi

uni-app跳转其他app

<template> <view class=“content”> <button @tap=“test”>跳转微信</button> </view></template><script>export default { data() { return { }; }, onReady: function() {}, methods: { test() { console.log(‘1’); var UIApplication = plus.ios.import(‘UIApplication’); var NSURL = plus.ios.import(‘NSURL’); var setting = NSURL.URLWithString(‘weixin://’); var application = UIApplication.sharedApplication(); application.openURL(setting); plus.ios.deleteObject(setting); plus.ios.deleteObject(application); }, }};</script><style></style>

January 2, 2019 · 1 min · jiezi

uni-app自定义导航icon字体库的问题

官方文档:一开始不是很理解,因为在这个平台 iconfont生成的unicode代码是&#xe604;这样的,根本用不了,不断测试下用把&#x改成\u就可以了1.首页在iconfont上传需要用的svg文件 iconfont使用教程官网有2.下载ttf字体文件:3.把ttf字体文件放到uni-app项目里面4.最终配置成功的uni-app导航栏配置:

December 27, 2018 · 1 min · jiezi

10分钟读懂阿里巴巴高级专家在Flutter Live2018的分享

12月4日,google flutter团队宣布第一个flutter正式版本发布。次日,Flutter Live Beijing 会议上,google flutter团队邀请了在这一技术方案中重要的合作伙伴闲鱼团队分享这半年以来的通过flutter产出的业务结果以及对应的技术挑战。本文根据Flutter Live Beijing嘉宾闲鱼客户端团队负责人于佳(宗心)的演讲内容进行整理,从flutter的优势和挑战引出闲鱼这半年来针对flutter基础设施进行重新的构建,定义,以及优化的过程,最后是这半年来对社区的一些贡献和未来的规划。Flutter的优势与挑战众所周知,Flutter提供了一套解决方案,既能用原生ARM代码直接调用的方式来加速图形渲染和UI绘制,又能同时运行在两大主流移动操作系统上,其像素级别的还原,保证了不同平台的UI强一致性。同时其提供了一整套机制(hot reload/attach Debug)保证开发的高效,基于此闲鱼团队在众多跨平台方案中选择了Flutter作为其未来主要的开发方案。从4月份开始尝试在业务侧接入flutter到现在,闲鱼在线上已经有10+的页面使用了flutter进行开发,其中覆盖了核心主链路发布和详情。闲鱼目前是市场上最大的闲置交易社区,作为一款有巨大用户体量的C端创新类产品,我们对体验以及研发迭代效率都有比较高的要求。在享受flutter带来的收益的过程中,同样会面临技术转型过程中的一些挑战。主要的挑战来源于以下的三个方面工程体系在现有工程体系下如何将flutter体系融入,并保持团队不同技术栈(Android/iOS/Flutter)的同学能各自独立高效进行开发。业务架构如何提供一套flutter之上的业务架构,保证上层代码的统一标准,同时尽可能的使得代码的复用度及隔离性更好。基础中间件如何保证不同技术栈背后使用的基础能力是一致的(底层统一使用具有相同优化策略的图片库/音视频库),且在这个过程中如何解决flutter融入后产生的问题。面对这些挑战,闲鱼团队在下半年开始了针对基础设施的改造与重建。基础设施重建之路工程体系工程体系部分,首当其冲需要考虑的是不同技术栈同学的协同问题,举例说明,我们的详情和发布页面是flutter的,而首页以及搜索部分目前暂时采用native进行开发。这就需要考虑到flutter的环境要对开发native的同学透明,甚至在native同学没有安装flutter环境的情况下,依然可以保持原来的方式进行开发native页面。如图中所示,以iOS为例,我们针对flutter的框架flutter.framewrok和业务代码App.framework通过持续集成服务进行打包并自动上传至云端的pod repo上,native同学只需在Podfile内指定对应的两个库的版本即可,同理,针对flutter的plugin代码,同样打包上传至pod repo即可。这套体系整体不复杂,需要说明的是,由于多人开发flutter工程,因此打包是一件非常频繁的事情,因此我们这半年构建了持续集成体系来帮助大家将打包上传等整个体系做成一键式服务,另外,由于原有iOS平台的flutter产物是需要依赖我们的native主工程的代码的,这种默认的打包方式,代码量巨大,造成持续集成时间在10-20分钟不等,因此在这个过程中,闲鱼团队通过直接基于xcode_backend.sh + insert_dylib的自定义脚本完成了不依赖native主工程源码的打包,将持续集成时间降至2分半。同理在android上面,也进行了一些基础的改造,感兴趣的同学可以给我们留言,我们会根据大家的需求程度在后续安排贡献给flutter社区。另外一部分比较重要的内容是混合栈相关的,由于flutter没有提供flutter到混合工程的最佳实践,所以我们在上半年自建了一整套混合栈的体系,这里主要是分享一些混合栈的关键思考,在混合栈的实现过程中,需重点测试验证dart这一侧widget的生命周期,并简化堆栈的管理(目前闲鱼的做法是将堆栈管理统一交由native进行控制,简化Dart层API),并需要考虑如何兼容Dart上层的比如Navigtor API的调用。整体这部分闲鱼团队还在验证当中,总之,这部分看似简单,但实际是比较深的坑,需要重点优化。另外,截至发文时间前,我们跟google flutter团队就混合栈交换了一些看法,flutter团队后续如果可以提供多flutterview,单flutter engine的基础能力,就可以使得闲鱼现有的混合栈实现成本整体大幅度降低,后续大家有什么特别好的建议,也欢迎跟我们进行交流。业务架构今年下半年由于有更多的业务迁移至flutter,这意味着更多的团队成员开始了Dart侧的研发。很快我们发现团队的代码风格,层次结构都比较混乱,bug也层出不穷,因此我们需要找到一种可以保证大家研发规范,同时确保多人协同过程中,代码既能更好的复用,又可以在适当的场景下做到相互隔离的这么一种方案。在经过社区的多个框架库的实践和比较以后,不管是flex还是redux都不能完全解决我们的问题。最后我们选择了自己进行设计和实现,我们对框架进行基础分层以后,将重点最终落在了基于单一数据源的组件化框架上面,因此我们产生了自己的框架fishRedux,我们严格参考标准js的redux规范和源码(redux的设计三原则)进行了完整的dart侧的实现,并在此基础上提供扩展能力用于我们的组件化开发。如图所示,component将redux中的view,reducer,middleware以及我们的扩展能力effect进行组装,从而可以在不同的页面进行组件的复用,当然,全局依然遵循redux的单一数据源的原则,但我们将逻辑本身通过更细粒度的拆解,保证了这些逻辑在不同的component组装下都可以尽可能的进行复用。基于这种结构,我们可以将任意的component进行挂载和拼装,通过更多小粒度的组件,产生不同场景下的复杂页面。另外,针对于component的多层组装,大家可以细看下dependents这个字段,通过基于这个字段的组装,在我们提供的这段代码里面,实际上是提供了一个详情页面的插槽的功能,详情页面目前在闲鱼有近10种不同的组合,在这个场景下,可以在保证组件可以服用的同时,做到不同流程下的代码隔离。我们只要针对dependents的components里面进行替换,就可以很容易的达到在详情页面插入不同widget以及逻辑的效果。fishRedux框架目前已经接近修改的尾声,目前还有部分微调和文档的补充,明年4月份前,我们有计划进行该框架的开源,为后续业务架构提供一个新的标准,大家敬请期待。基础中间件在阿里集团内部,已经产出了较多的基础中间件,因此如何复用这个中间件到flutter侧是一个新的挑战,针对于传统的比如网络库,crash收集等中间件,由于不涉及到UI的复用,相对容易,但针对音视频,图片库等这类的中间件,虽然flutter提供了flutterTexture的方案,但依然不是特别完美。我们在做音视频及图片库的复用过程中,主要的问题在于flutter原生提供的机制,针对图片的渲染存在GPU到CPU,然后CPU再到GPU的这样一个过程,如图所示。根本原因在于不同的glContext无法共享texture。因此,我们目前采取的方案是修改flutter引擎,并暴露出glContext的shareGroup(以iOS为例)。目前整个方案已经上线。由于该改动目前在闲鱼自己fork的engine里面,因此目前将我们之前踩到的一些坑同步给大家,如果大家有在flutter和native侧同时使用音视频的情况,建议特别注意ppt中的前两点,否则会造成flutter侧或者native侧音视频的错乱。当然如果按照闲鱼团队的提供的修改方案进行engine改造后,也可以通过第三点,对native设置跟flutter相同的sharegroup来解决这个问题。在flutter live Beijing结束之后,我也将该方案正式介绍给google flutter团队,希望后续能将类似的功能融入flutter的官方实现。闲鱼与flutter社区闲鱼这半年,对于flutter社区,也有一些小小的贡献。我们针对flutter的方案进行整理并在各个技术社区进行传播。另外我们将已有的一些问题和解决方案提供给google flutter官方团队,直接或者间接的推动了flutter的整体进度,并改变了这个技术未来的部分走向。我为自己的团队感到由衷的骄傲,但同时我意识到,要想让flutter成为终端未来的主流技术,依然任重道远,因此我们后续也会将目前的一些相对稳定的框架和解决方案,逐步开源到社区,我们的要求是,至少闲鱼团队需要在线上有应用和验证。目前我们已经有一些初步的demo和小工具放在上面,大家感兴趣的可以往我们的github上提issue,我们后续会逐步开放更多的代码。最近会公布的比较重要的框架会是fishRedux,因此请大家持续关注我们。总结与展望我们首先带大家回顾了flutter带来的优势以及在闲鱼的实际例子,并引出在复杂工程下的一些挑战。我们针对这些挑战,在下半年进行了整个体系的重新建设,初步完成了隔离的混合工程体系统一标准的业务架构高效复用基础中间件设施本次的分享,其实只是我们目前团队的一部分内容,我们基于flutter和dart还有更多的技术方案目前在预研和研发中,所以没有在这次live中进行宣讲。在后续跟google flutter团队的沟通中也了解到,他们对我们的多个方案都有较大的兴趣。对于未来来说,一方面,除了本文分享的内容以外,我们自己在代码自动生成/Dart Server/线上问题自动回放/国际化/动态模版等/持续集成等多个方面都在持续关注和调研。另一方面,在flutter 1.0公布后,类似hummingbrid这一类全新的方案也有机会让flutter具有全终端制霸的可能性,我们也会持续跟进和进行尝试。Anyway,依然希望有更多的同学可以加入我们一起完善flutter的生态,同时通过新的技术手段,让天下没有闲置。来闲鱼一起改变世界吧,少年!PPT下载:https://yq.aliyun.com/download/3130本文作者:闲鱼技术-宗心.阅读原文本文为云栖社区原创内容,未经允许不得转载。

December 11, 2018 · 1 min · jiezi

做了2个多月的设计和编码,我梳理了Flutter动态化的方案对比及最佳实现

背景在端上为了提升App的灵活性, 快速解决万变的业务需求,开发者们探索了多种解决方案,如PhoneGap ,React Native ,Weex等,但在Flutter生态还没有好的解决方案。未来闲鱼都会基于Flutter 来跨端开发,如果突破发版周期,在不发版的情况下,完成业务需求,同时能兼容性能体验,无疑是更快的响应了业务需求。因此我们需要探索在Flutter生态下的动态化。方案选择借鉴Android 和Ios上的动态性方案,我们也思考了多种Flutter动态性方案。1.下载替换Flutter编译产物下载新的Flutter编译产物,替换 App 安装目录下的编译产物,来实现动态化,这在Android 端是可行的,但在Ios 端不可行。我们需要双端一体的解决方案,所以这不是最好选择。2.类似React Native 框架我们先来看看React Native 的架构React Native 要转为android(ios) 的原生组件,再进行渲染。用React Native的设计思路,把XML DSL转为Flutter 的原子widget组件,让Flutter 来渲染。技术上说是可行的,但这个成本很大,这会是一个庞大的工程,从投入产出比看,不是很好的选择3.页面动态组件框架由粗粒度的Widget组件动态拼装出页面,Native端已经有很多成熟的框架,如天猫的Tangram,淘宝的DinamicX,它在性能、动态性,开发周期上取得较好平衡。关键它能满足大部分的动态性需求,能解决问题。三种方案的比较图表如:根据实际动态性需求,从两端一致性,和性能,成本,动态性考虑,我们选择一个折中方案,页面动态组件的设计思路是一个不错的选择。页面动态组件框架在Flutter上使用粗力度的组件动态拼装来构建页面,需要一整套的前后端服务和工具。本文我们重点介绍前端界面渲染引擎过程。语法树的选择Native端的Tangram ,DinamicX等框架他们有个共同点,都是Xml或者Html 做为DSL。但是Flutter 是React Style语法。他自己的语法已经能很好的表达页面。无需要自定义的Xml 语法,自定义的逻辑表达式。用Flutter 源码做为DSL 能大大减轻开发,测试过程,不需要额外的工具支持。所以选择了Flutter 源码作为DSL,来实现动态化。如何解析DSLFlutter源码做为DSL,那我们需要对源码进行很好的解析和分析。Flutter analyzer给了我们一些思路,Flutter analyzer是一个代码风格检测工具。它使用package:analyzer来解析dart 源码,拿到ASTNode。看下Flutter analyze 源码结构,它使用了dart sdk 里面的 package:analyzerdart-sdk: analysis_server: analysis_server.dart handleRequest(Request request) analyzer: parseCompilationUnit() parseDartFile parseDirectives Flutter analyze 解析源码得到ASTNode过程。插件或者命令对analysis server发起请求,请求中带需要分析的文件path,和分析的类型,analysis_server经过使用 package:analyzer 获取 commilationUnit (ASTNode),再对astNode,经过computer分析,返回一个分析结果list。同样我们也可以把使用 package:analyzer 把源文件转换为commilationUnit (ASTNode),ASTNode是一个抽象语法树,抽象语法树(abstract syntax tree或者缩写为AST)是源代码的抽象语法结构的树状表现形式.所有利用抽象语法树能很好的解析dart 源码。解析渲染引擎下面重点介绍渲染模块架构图:1.源码解析过程1.AST树的结构如下面这段Flutter组件源码:import ‘package:flutter/material.dart’;class FollowedTopicCard extends StatelessWidget { @override Widget build(BuildContext context) { return new Container( padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 0.0), child: new InkWell( child: new Center( child: const Text(‘Plugin example app’), ), onTap: () {}, ), ); }}它的AST结构:从AST结构看,他是有规律的.2.AST 到widget Node我们拿到了ASTNode,但ASTNode 和widget node tree 完全是两个不一样的概念,需要递归ASTNode 转化为 widget node tree.widget Node 需要的元素用Name 来记录是什么类型的widgetwidget的arguments放在 map里面widget 的literals 放在list 里面widget 的children 放在lsit 里面widget 的触发事件 函数map里面widget node 加fromjson ,tojson 方法可以在递归astNode tree 时候,识别InstanceCreationExpression来创建一个widget node。2.组件数据渲染框架sdk 中注册支持的组件,组件包括:a.原子组件:Flutter sdk 中的 Flutter 的widgetb.本地组件:本地写好到一个大颗粒的组件,卡片widget组件c.逻辑组件:本地包装了逻辑的widget组件d.动态组件:通过源码dsl动态渲染的widget具体代码如下: const Map<String, CreateDynamicApi> allWidget = <String, CreateDynamicApi>{ ‘Container’: wrapContainer, ………….} static Widget wrapContainer(Map<String, dynamic> pars) { return new Container( padding: pars[‘padding’], color: pars[‘color’], child: pars[‘child’], decoration: pars[‘decoration’], width: pars[‘width’], height: pars[‘height’], alignment: pars[‘alignment’] );}一般我们通过网络请求拿到的数据是一个map。比如源码中写了这么一个 ‘${data.urls[1]}‘AST 解析时候,拿到这么一个string,或者AST 表达式,通过解析它 ,肯定能从map 中拿到对应的值。3.逻辑和事件a.支持逻辑Flutter 概念万物都是widget ,可以把表达式,逻辑封装成一个自定义widget。如果在源码里面写了if else,变量等,会加重sdk解析的过程。所以把逻辑封装到widget中。这些逻辑widget,当作组件当成框架组件。b.支持事件把页面跳转,弹框,等服务,注册在sdk里面。约定使用者仅限sdk 的服务。4.规则和检测工具a.检测规则需要对源码的格式制定规则。比如不支持 直接写if else ,需要使用逻辑wiget组件来代替if else 语句。如果不制定规则,那ast Node 到widget node 的解析过程会很复杂。理论上都可以解析,只要解析sdk 够强大。制定规则,可以减轻sdk的解析逻辑。b.工具检测用工具来检测源码是否符合制定的规则,以保证所有的源码都能解析出来。性能和效果帧率大于50fps,体验上看比weex相同功能的页面更加流畅,Samsung galaxy s8上,感觉不出组件是通过动态渲染的.数据结构服务端请求到的数据,我们可以约定一种格式如下:class DataModel { Map<dynamic, dynamic> data; String type;}每个page 都是由组件组成的,每个组件的数据都是 DataModel来渲染。根据type 来找到对应的模版,模版+data,渲染出界面。动态模版管理模块我们把Widget Node Tree 转换为一个组件Json模版,它需要一套管理平台,来支持版本控制,动态下载,升级,回滚,更新等。框架的边界该框架是通过组件的组装,组件布局动态变更,页面布局动态变更来实现动态化。所以它适合运营变化较快的首页,详情,订单,我的等页面。一些复杂的逻辑需要封装在组件里面,把组件内置到框架中,当作本地组件。框架侧重于动态组件的组装,而引擎对于源码复杂的逻辑表达式的解析是弱化的。后续拓展1.和UI自动化的结合UI自动化 ,前面已经有文章介绍。UI自动化工具生成组件,再组件转为模版,动态下发,来快速解决运营需求。2.国际化的支持App在不同国家会有不同的功能,我们可以根据区域,来动态拼装我们的页面。3.千人千面根据不同的人群,来动态渲染不一样的界面。总结本文介绍动态化方案的渲染部分。该方案都在初探阶段,还有很多需要完善,后续会继续扩展和修改,等达到开源标准后,会考虑开源。动态方案是一个后端前端一体的方案,需要一整套工具配合,后续会有文章继续介绍整体的动态化方案。敬请关注闲鱼技术公共账号,也邀请您加入闲鱼一起探索有意思的技术。参考资料:Static Analysis:https://www.dartlang.org/guides/language/analysis-optionshttps://www.dartlang.org/tools/analyzerdart analyzer :https://pub.dartlang.org/packages/analyzerhttps://github.com/dart-lang/sdk/tree/master/pkg/analyzer_cli#dartanalyzerdartdevc:https://webdev.dartlang.org/tools/dartdevc本文作者:闲鱼技术-石磬阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 5, 2018 · 1 min · jiezi

在Flutter中嵌入Native组件的正确姿势是...

引言在漫长的从Native向Flutter过渡的混合工程时期,要想平滑地过渡,在Flutter中使用Native中较为完善的控件会是一个很好的选择。本文希望向大家介绍AndroidView的使用方式以及在此基础之上拓展的双端嵌入Native组件的解决方案。1. 使用教程1.1. DemoRun嵌入地图这一场景可能在很多App中都会存在,但是现在的地图SDK都没有提供Flutter的库,而自己开发一套地图显然不太现实。这种场景下,使用混合栈的形式是一个比较好的选择。我们可以直接在Native的绘图树中嵌入一个Map,但是这个方案嵌入的View并不在Flutter的绘图树中,是一种比较暴力且不优雅的方式,使用起来也很费劲。这时候,使用Flutter官方提供的控件AndroidView就是一种比较优雅的解决方案了。这里做了一个简单的嵌入高德地图的demo,就让我们跟着这个应用场景,看一下AndroidView的使用方式和实现原理。1.2. AndroidView使用方式AndroidView的使用方式和MethodChannel类似,比较简单,主要分为三个步骤:第一步:在dart代码的相应位置使用AndroidView,使用时需要传入一个viewType,这个String将用于唯一标识该Widget,用于和Native的View建立关联。第二步:在native侧添加代码,写一个PlatformViewFactory,PlatformViewFactory的主要任务是,在create()方法中创建一个View并把它传给Flutter(这个说法并不准确,但是我们姑且可以这么理解,后续会进行解释)第三步:使用registerViewFactory()方法注册刚刚写好的PlatformViewFactory,该方法需要传入两个参数,第一个参数需要和之前在Flutter端写的viewType对应,第二个参数是刚刚写好的的PlatformViewFactory。配置高德地图的部分这里就省略不说了,官方有比较详细的文档,可以去高德开发者平台进行查阅。以上便是使用AndroidView的所有操作,总体看起来还是比较简单的,但是真正要用起来,还是有两个无法忽视的问题:View最终的显示尺寸由谁决定?触摸事件是如何处理的?下面就让小闲鱼来给各位一一解答。2. 原理讲解想要解决上面的两个问题,首先必须得理解所谓"传View"的本质是什么?2.1. 所谓"传View"的本质是什么?要解决这个问题,自然避免不了的需要去阅读源码,从更深的层面去看这个传递的整个过程,可以整理出一张这样的流程图:我们可以看到,Flutter最终拿到的是native层返回的一个textureId。根据native的知识ky h这个textureId是已经在native侧渲染好了的view的绘图数据对应的ID,通过这个ID可以直接在GPU中找到相应的绘图数据并使用,那么Flutter是如何去利用这个ID的呢?在之前的深入了解Flutter界面开发中,也给大家介绍了Flutter的绘图流程。我这里也给大家再简单整理一下Flutter的Framework层最后会递交给Engine层一个layerTree,在管线中会遍历layertree的每一个叶子节点,每一个叶子节点最终会调用Skia引擎完成界面元素的绘制,在遍历完成后,在调用glPresentRenderBuffer(IOS)或者glSwapBuffer(Android)按完成上屏操作。Layer的种类有很多,而AndroidView则使用的是其中的TextureLayer。TextureLayer在之前的《Flutter外接纹理》中有更为详细的介绍,这里就不再赘述。TextureLayer在被遍历到时,会调用一个engine层的方法SceneBuilder::addTexture() 将textureId作为参数传入。最终在绘制的时候,skia会直接在GPU中根据textureId找到相应的绘制数据,并将其绘制到屏幕上。那么是不是谁拿到这个ID都可以进行这样的操作呢?答案当然是否定的,Texture数据存储在创建它的EGLContext对应的线程中,所以如果在别的线程进行操作是无法获取到对应的数据的。这里需要引入几个概念:显示屏对象(Display):提供合理的显示器的像素密度和大小的信息Presentation:它给Android提供了在对应的上下文(Context)和显示屏对象(Display)上绘制的能力,通常用于双屏异显。这里不展开讲解Presentation,我们只需要明白Flutter是通过Presentation实现了外接纹理,在创建Presentation时,传入FlutterView对应的Context和创建出来的一个虚拟显示屏对象,使得Flutter可以直接通过ID找到并使用Native创建出来的纹理数据。2.2. View最终的显示尺寸由谁决定?通过上面的流程大家应该都能想到,显示尺寸看起来像是由两部分决定的:AndroidView的大小,Android端View的大小。那么实际上到底是有谁来决定的呢,让我们来做一个实验?直接新建一个Flutter工程,并把中间改成一个AndroidView。//Flutterclass _MyHomePageState extends State<MyHomePage> { double size = 200.0; void _changeSize() { setState(() { size = 100.0; }); } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text(widget.title), ), body: Container( color: Color(0xff0000ff), child: SizedBox( width: size, height: size, child: AndroidView( viewType: ’testView’, ), ), ), floatingActionButton: new FloatingActionButton( onPressed: _changeSize, child: new Icon(Icons.add), ), ); }}在Android端也要加上对应的代码,为了更好地看出裁切效果,这里使用ImageView。//Android@Overridepublic PlatformView create(final Context context, int i, Object o) { final ImageView imageView = new ImageView(context); imageView.setLayoutParams(new ViewGroup.LayoutParams(500,500)); imageView.setBackground(context.getResources().getDrawable(R.drawable.idle_fish)); return new PlatformView() { @Override public View getView() { return imageView; } @Override public void dispose() { } };}首先先看AndroidView,AndroidView对应的RenderObject是RenderAndroidView,而一个RenderObject的最终大小的确定是存在两种可能,一种是由父节点所指定,还有一种是在父节点指定的范围中根据自身情况确定大小。打开对应的源码,可以看到其中有个很重要的属性sizedByParent = true,也就是说AndroidView的大小是由其父节点所决定的,我们可以使用Container、SizedBox等控件控制AndroidView的大小。AndroidView的绘图数据是Native层所提供的,那么当Native中渲染的View的实际像素大小大于AndroidView的大小时,会发生什么呢?通常情况下,这种情况的处理思路无非就两种选择,一种是裁切,另一种是缩放。Flutter保持了其一贯的做法,所有out of the bounds的Widget统一使用裁切的方式进行展示,上面所描述的情况就被当作是一种out of the bounds。当这个View的实际像素大小小于AndroidView的时候,会发现View并不会相应地变小(Container的背景色并没有显露出来),没有内容的地方会被白色填充。这其中的原因是SingleViewPresentation::onCreate中,会使用一个FrameLayout作为rootView。2.3. 触摸事件如何传递Android的事件流大家应该都很熟悉了,自顶向下传递,自底向上处理或回流。Flutter同样是使用这一规则,但是其中AndroidView通过两个类来去处理手势:MotionEventsDispatcher:负责将事件封装成Native的事件并向Native传递;AndroidViewGestureRecognizer:负责识别出相应的手势,其中有两个属性:cachedEvents和forwardedPointers,只有当PointerEvent的pointer属性在forwardedPointers中时才会去进行分发,否则会存在cacheEvents中。这里的实现主要是为了解决一些事件的冲突,比如滑动事件,可以通过gestureRecognizers来进行处理,这里可以参考官方注释。/// For example, with the following setup vertical drags will not be dispatched to the Android view as the vertical drag gesture is claimed by the parent [GestureDetector]./// /// GestureDetector(/// onVerticalDragStart: (DragStartDetails d) {},/// child: AndroidView(/// viewType: ‘webview’,/// gestureRecognizers: <OneSequenceGestureRecognizer>[],/// ),/// )/// /// To get the [AndroidView] to claim the vertical drag gestures we can pass a vertical drag gesture recognizer in [gestureRecognizers] e.g:/// /// GestureDetector(/// onVerticalDragStart: (DragStartDetails d) {},/// child: SizedBox(/// width: 200.0,/// height: 100.0,/// child: AndroidView(/// viewType: ‘webview’,/// gestureRecognizers: <OneSequenceGestureRecognizer>[ new VerticalDragGestureRecognizer() ],/// ),/// ),/// )所以总结起来,这部分流程总结起来其实也很简单:事件最初从Native到Flutter这一阶段不在本文的讨论范围之内,Flutter按照自己的规则去处理事件,如果AndroidView赢得了事件,事件就会被封装成相应的Native端的事件并且通过方法通道传回Native,Native再根据自己的处理事件的规则去处理。3. 总结3.1. 方案局限性往大里说:这套方案是Google为了解决开发者日益增长的业务需求与落后的生态环境之间的矛盾而产生的,这一矛盾是一个新生态必然需要去面对的主要矛盾。为了解决这一个问题,最简单的方式当然就是允许开发者使用老生态中已经非常成熟的控件。当然,这样是可以临时解决Flutter生态发展不全面的问题,但是使用这套方案不可避免的需要去编写双端代码(甚至现在iOS还没有对应的控件,当然之后肯定会更新),不能做到真正的跨端。往小里说:这套方案存在着性能上的缺陷,在AndroidView这个类的第三句注释中,官方就已经提到了这是一套比较昂贵的方案,避免在使用Flutter控件也能实现的情况下去使用它。如果之前有看过《Flutter外接纹理》这一文章的同学应该知道,Flutter实现外接纹理的方案中,数据从GPU->CPU->GPU的过程代价是比较大的,在大量使用的场景会造成明显的性能缺陷。我们通过一些手段绕过了中间CPU这一步,并且将这项技术在APP中落地,用于处理图片资源。3.2. 实际应用目前闲鱼从Native向Flutter的迁移工作遇到了Native的本地图片资源在Flutter侧无法访问的问题,在现在Flutter和Native必将长期共存的情况下,重新拷贝一份资源以Flutter的规则来存储当然可以,但是不可避免地增大了包体积,而且不好管理。面对这个问题,我们的解法便是借鉴了AndroidView使用Texture的思路并在将其优化。实现了Native和Flutter的图片资源归一化。除了用于加载位于Native资源目录下的本地图片之外,还可以利用Native的图片库来加载网络图片。我们这么去做的原因是我们在Native侧的图片库较为完善并且经受过大量的线上考验,现在这一阶段,我们不希望将过多的精力投入到重复造轮子这一件事上,而处理网络图片资源和处理本地图片资源的思路其实是一样的,所以我们选择将图片资源进行了统一地整合,在与官方的团队进行沟通并完善后会和大家同步,敬请关注我们的公众号。3.4. 引用高德地图SDK文档万万没想到——Flutter外接纹理Android7.1 Presentation双屏异显原理分析本文作者:闲鱼技术-尘萧阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

November 16, 2018 · 1 min · jiezi