Android Flutter实践内存初探

摘要:Android Flutter实践内存初探 闲鱼技术-匠修我们想使用Flutter来统一移动App开发并做了一些实践。移动设备上的资源有限,通常内存使用都是一个我们日常开发中十分关注的问题。那么,Flutter是如何使用内存,又会对Native App的内存带来哪些影响呢?本文将简单介绍Flutter内存机制,结合测试和我们的开发实践,对日常关心的Bitmap内存使用,View绘制内存使用方面做一些探索。

闲鱼技术-匠修

我们想使用Flutter来统一移动App开发并做了一些实践。移动设备上的资源有限,通常内存使用都是一个我们日常开发中十分关注的问题。那么,Flutter是如何使用内存,又会对Native App的内存带来哪些影响呢?本文将简单介绍Flutter内存机制,结合测试和我们的开发实践,对日常关心的Bitmap内存使用,View绘制内存使用方面做一些探索。

Dart RunTime简介

Flutter Framework使用Dart语言开发,所以App进程中需要一个Dart运行环境(VM),和Android Art一样,Flutter也对Dart源码做了AOT编译,直接将Dart源码编译成了本地字节码,没有了解释执行的过程,提升执行性能。这里重点关注Dart VM内存分配(Allocate)和回收(GC)相关的部分。

和Java显著不同的是Dart的"线程"(Isolate)是不共享内存的,各自的堆(Heap)和栈(Stack)都是隔离的,彼此之间通过消息通道来通信。Dart天然不存在数据竞争和变量状态同步的问题,整个Flutter Framework Widget的渲染过程都运行在一个isolate中。

Dart VM将内存管理分为新生代(New Generation)和老年代(Old Generation)。

新生代(New Generation): 通常初次分配的对象都位于新生代中,该区域主要是存放内存较小并且生命周期较短的对象,比如局部变量。新生代会频繁执行内存回收(GC),回收采用“复制-清除”算法,将内存分为两块(图中的from 和 to),运行时每次只使用其中的一块(图中的from),另一块备用(图中的to)。当发生GC时,将当前使用的内存块中存活的对象拷贝到备用内存块中,然后清除当前使用内存块,最后,交换两块内存的角色。

老年代(Old Generation): 在新生代的GC中“幸存”下来的对象,它们会被转移到老年代中。老年代存放生命力周期较长,内存较大的对象。老年代通常比新生代要大很多。老年代的GC回收采用“标记-清除”算法,分成标记和清除两个阶段。在标记阶段,所有线程参与并发的完成对回收对象的标记,降低标记阶段耗时。在清理阶段,由GC线程负责清理回收对象,和应用线程同时执行,不影响应用运行。

可以看到,Dart VM借鉴了很多JVM的思路,Dart中产生内存泄露的方式也和Java类似,Java中很多排查内存泄露的思路和防止内存泄露的编程方法应该也可以借鉴过来。

Image内存初探

对图片的合理使用和优化是UI编程的重要部分,Flutter提供了Image Widget,我们可以方便的使用:

//使用本地图片newImage.asset("images/xxxx.jpg");//使用网络图片newImage.network("https://xxxxxx");

我们知道Android将内存分为Java虚拟机内存和Native内存,各大厂商都对Java虚拟机内存有一个上限限制,到达上限就会触发OOM异常,而对Native内存的使用没有太严格的限制,现在的手机内存都很大,一般有较大的Native内存富余。那么Android中ImageView使用的是Java虚拟机内存还是Native内存呢?

我们可以来做一个测试:在一个界面上,每点击一次,就在上面堆加一张图片。为了防止后面的图片完全覆盖前面的图片而出现优化的情况,每次都缩小几个像素,这样就不会出现完全覆盖。

打开Android Profiler,一张一张添加图片,观察内存数据。分别测试了Android的6.0,7.0和8.0系统,结果如下:

Android 6.0(Google Nextus5)

Android 7.0(Meizu pro5)

Android 8.0(Google pixel)

在测试中,随着图片一张张增加,Android 6.0 和 7.0都是Java部分的内存在增长,而Android 8.0则是Native部分的内存在增长。由此有结论,Android原生的ImageView在6.0和7.0版本中使用的Java虚拟机内存,而在Android 8.0中则使用的Native内存。

而Flutter Image Widget使用的是哪部分内存呢?我们用Flutter界面来做相同的测试。Flutter Engine的Debug版本和Release版本存在很大的性能差异,所以我们测试最好使用Release版本,但是,Release版本的Apk又不能使用Android profiler来观察内存,所以我们需要在Debug版本的Apk中打包一个Release版本的Flutter Engine, 可以修改flutter tool中的flutter.gradle来实现:

//不做判断,强制改为打包release版本的engineprivatestaticStringbuildModeFor(buildType) {    //if(buildType.name =="profile") {    //return"profile"// }elseif(buildType.debuggable) {    //return"debug"// }return"release"}

相同地,我们向Flutter界面中添加图片并用Android Profiler来观察内存,测试使用的dart代码:

classStackImageStateextendsState{varimages = [];varindex =0;  @override  Widget build(BuildContext context) {varwidgets = [];for(int i =0; i <= index; i++) {varpos = i - (i ~/103) *103;      widgets.add(newContainer(          child:newImage.asset("images/${pos}.jpg", fit: BoxFit.cover),          padding:newEdgeInsets.only(top: i *2.0)));    }    widgets.add(newCenter(        child:newGestureDetector(            child:newContainer(                child:newText("添加图片(${index})",                    style:newTextStyle(color: Colors.red)),                color: Colors.green,                padding:constEdgeInsets.all(8.0)),            onTap: () {              setState(() {                index++;              });            })));returnnewStack(        children: widgets, alignment: AlignmentDirectional.topCenter);  }}

得到的结果是:

Android 6.0

Android 8.0

可以看到,Flutter Image使用的内存既不属于Java虚拟机内存也不属于Native内存,而是Graphics内存(在Meizu pro5设备上也不属于Graphics,事实上Meizu pro5设备不能归类Flutter Image所使用的内存),官方对Graphics内存的解释是:

那么至少Flutter Image所使用的内存不会是Java虚拟机内存,这对不少Android设备都是一个好消息,这意味着使用Flutter Image没有OOM的风险,能够较好的利用Native内存。

使用Image的时候,建立一个内存缓存池是个好习惯,Flutter Framework提供了一个ImageCache来缓存加载的图片,但它不同于Android Lru Cache,不能精确的使用内存大小来设定缓存池容量,而是只能粗略的指定最大缓存图片张数。

FlutterView内存初探

Flutter设计之初是想统一Android和IOS的界面编程,所以理想的基于Flutter的apk只需要提供一个MainActivity做入口即可,后面所有的页面跳转都在FlutterView中管理。但是,如果是一个已有规模的app接入Flutter开发,我们不可能将已有的Activity页面都用Flutter重新实现一遍,这时候就需要考虑本地页面和Flutter页面之间的跳转交互了。iOS可以方便的管理页面栈,但是Android就很复杂(Android有任务栈机制,低内存Activity回收机制等),所以通常我们还是使用Activity作为页面容器来展示flutter页面。这时有两种选择,可以每次启动一个Activity就启动一个新的FlutterView,也可以启动Activity的时候复用已有的FlutterView。

不复用FlutterView

复用FlutterView

Flutter Framework中FlutterView是绑定Activity使用的,要复用FlutterView就必须能够把FlutterView单独拎出来使用。所幸现在FlutterView和Activity耦合程度并不很深,最关键的地方是FlutterNativeView必须attach一个Activity:

//attach到当前ActivitymNativeView.attachViewAndActivity(this, activity);

初始化FlutterView时必须传入一个Activity,当其他Activity复用FlutterView时再调用该Attach方法即可。这里有个问题,就是FlutterView中必须保存一个Activity引用,这个一个内存泄露隐患,我们可以在FluterView detach时候将MainActivity传入,因为通常整个App交互过程中MainActivity都是一直存在的,可以避免其他Activity泄露。

为了更好的权衡两种方法的利弊,我们先用空页面来测试一下当页面增加时内存的变化:

不复用FlutterView时,页面增加时内存变化

复用FlutterView时,页面增加时内存变化

不复用FlutterView时平均打开一个页面(空页面),Java内存增长0.02M,Native内存增长0.73M。复用FlutterView时平均打开一个页面(空页面),Java内存增长0.019M,Native内存增长0.65M。可见复用FlutterView在内存使用上是有优势的,但主要复用的还是Native部分的内存。复用FlutterView必然带来额外的一些复杂逻辑,有时候为了逻辑简单,后期维护上的方便,牺牲一些相对不太珍贵的Native内存也是值得的。

复用单个FlutterView有时会有些“意外”,比如当Activity切换时,就不得不将当前FlutterView detach掉给后面新建的Activity使用,当前界面就会空白闪动,有个想法是可以将当前界面截屏下来遮挡住后面的界面变化,这种方式有时会带来额外的适配问题。

FlutterView复用与否不是绝对的,有时候可以使用一些综合性折中方案,比如,我们可以建立一个FlutterViewProvider,里面维护N个可复用的FlutterView,如图:

这样的好处是,可以存在一定程度上的复用,又可以避免只有一个FlutterView出现的一些尴尬问题。

FlutterView的首帧渲染耗时较高,在Debug版本有明显感受,大概会黑屏2秒,release版本会好很多。但我们观察Cpu曲线,发现还是一个较为耗时的过程。有一种体验优化的思路是,我们可以预先让将要使用的FlutterView加载好首帧,这样,在真正使用的时候就很快了,可以先建立一个只有1个像素的窗口,在这个窗口里面完成FlutterView首帧渲染,代码如下:

finalWindowManager wm = mFakeActivity.getWindowManager();finalFrameLayout root =newFrameLayout(mFakeActivity);//一个像素足矣FrameLayout.LayoutParams params =newFrameLayout.LayoutParams(1,1);root.addView(flutterView,params);WindowManager.LayoutParams wlp =newWindowManager.LayoutParams();wlp.width =1;wlp.height =1;wlp.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;wlp.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;wm.addView(root,wlp);finalFlutterView.FirstFrameListener[] listenerRef =newFlutterView.FirstFrameListener[1];    listenerRef[0] =newFlutterView.FirstFrameListener() {@OverridepublicvoidonFirstFrame(){//首帧渲染完后取消窗口wm.removeView(root);          flutterView.removeFirstFrameListener(listenerRef[0]);      }      };flutterView.addFirstFrameListener(listenerRef[0]);String appBundlePath = FlutterMain.findAppBundlePath(mFakeActivity.getApplicationContext());flutterView.runFromBundle(appBundlePath,null,"main",true);

以上就是闲鱼团队在Flutter的应用过程中的一些实践,希望有更多的新技术尝试和技术挑战的同学,请在下面留言联系我们哟~

简历投递:guicai.gxy@alibaba-inc.com

原文链接

原文链接

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,424评论 25 707
  • 导读:闲鱼技术团队一直在探索如何使用Flutter来统一移动App开发。移动设备上的资源有限,内存使用成了日常开发...
    盖世英雄_ix4n04阅读 585评论 0 4
  • 流年带走了青春,徒留回忆,拒绝怀旧,因为太过伤感。但我生来就是一个喜欢怀旧的人,不论心中花田的过往是繁华亦...
    木语MH阅读 1,141评论 3 11
  • 一座小城,一对夫妻,一双儿女,三个小家。 对,没错,我今天要讲的就是一个大家的故事。 这个大家里,三代同堂,只是同...
    十八般文艺人阅读 312评论 0 0
  • 两天的日立培训结束了,也拿到了合格证书。收获很大,日企中,日立还算是比较厚道的。昨天,读了关于暖通的资料,写了一篇...
    国庆_b919阅读 149评论 0 0