首页 文章 精选 留言 我的

精选列表

搜索[计算机组成原理],共10000篇文章
优秀的个人博客,低调大师

数据平台与Flink任务运行原理介绍

本文将从5个方面详细介绍数据平台大数据任务开发的基础知识。包含数据平台的简介、如何在平台内创建Flink表、如何编写Flink大数据处理作业、Flink任务启动时的调用流程以及Flink web UI 任务监控页面。此外,将重点介绍数据平台内Flink任务开发相关的部分内容,包括Flink数据连接、Flink流作业、批作业和资源库。资源库为自定义的UDF,为了方便介绍后续将统一简称为平台。 云智慧AIOps社区是由云智慧发起,针对运维业务场景,提供算法、算力、数据集整体的服务体系及智能运维业务场景的解决方案交流社区。该社区致力于传播AIOps技术,旨在与各行业客户、用户、研究者和开发者们共同解决智能运维行业技术难题、推动AIOps技术在企业中落地、建设健康共赢的AIOps开发者生态。 数据平台 平台定义 平台基于Flink计算框架,集成Kafka、Clickhouse等大数据组件,具备数据采集、数据同步、数据存储、实时计算、离线计算、数据治理、数据服务等能力。实践平台化思维,充分利用数据,可实现复杂业务的数据开发任务,构建了一套完整的技术生态体系。 平台目标 定位为实时数仓、流批一体、湖仓一体的智能大数据平台,提升整体的IT运营服务能力。 Flink数据连接 Flink数据连接是Flink读取和写入到web系统数据库的一个接口。下图为Flink 数据连接的一个页面,左侧的为目录数,方便数据源的归类与查找。右侧列表化展示数据连接,可根据数据连接的名称、类型、标签等内容进行检索,支持导入、导出与复制,方便做迁移和备份编辑。右上角新建按钮可以点击新建Kafka、Redis以及Clickhouse 的原表和结果表。通过页面式、配置化操作,将各数据资源虚拟化成Flink表。 新建Kafka源表 命名数据连接名称:名称方便于在数据列表中进行查询。 地址和端口:用于读取集群上的topic; Kafka类型:Kafka类型分为Kafka和 upsert-Kafka; 数据格式:常用数据格式为json; 消费方式:分为从头开始消费(从最老的数据将Kafka topic完整的数据都消费到)、从当前位置,从group offset以及从指定时间开始消费; 最大读取记录数:一批次内读取的最大数据量; key格式与字段 动态表名称:后续在Flink SQL中写SQL时将会用到,即建表的表名 Kafka源表创建完成之后,将会得到该建表语句 支持数据连接类型 Kafka:支持Kafka连接器和upsert-Kafka连接器 Clickhouse:基于jdbc连接器开发,支持Clickhouse集群和多节点轮询数据写入 Redis:自主开发,支持单机、集群、哨兵和代理模式的Redis服务,提供写入和维表关联能力 逐步支持更多的数据连接 数据作业开发 数据作业开发页面与数据源页面类似,左边为目录,右边可以新建编辑作业。 编辑SQL页面支持以下操作: 支持勾选数据源表和数据结果表 选择数据表后,填充建表语句至编辑区,支持自定义修改 支持添加自定义算子 SQL编辑完成后可进行作业配置操作: 并行度:任务每个算子的线程数 插槽数:每个插槽独占CPU资源,但使用同一个执行器的JVM内存资源(同一个taskmanager的插槽),增加插槽数会获取更多的计算资源,但不利于内存资源的利用效率。 重启策略:可设置固定重启次数和失败比例策略 任务启动流程 Flink任务启动会运行起一个JobManager和一个或多个TaskManager。 JobManager:负责资源提供、回收、分配,任务调度、处理完成或失败的任务、协调 checkpoint、提供REST接口和WebUI展示作业执行信息。 TaskManager:执行作业任务并缓存和交换数据流,包含一个或多个task slot。TaskManager 中 task slot 的数量表示并发处理 task 的数量,一个 task slot 中可以执行多个算子。 TaskManager是一个JVM进程,所包含的多个Task Slot会均分JVM内存。 多个算子可共享一个Task Slot,有利于资源分配和资源利用,并行度设置为Task Slot数的整数倍,会均匀的分配到每个Task Slot。 Flink任务监控页面 平台内维护了任务状态、运行日志等内容,Flink自身也提供了详细的任务监控能力,Flink任务监控包含Flink任务运行状态、时长以及资源分配情况的监控。会展示算子DAG图,数据处理情况以及算子反压、异常日志、Checkpoint记录等。 下图为TaskManager、JobManager的内存占用、GC情况以及运行日志。 了解更多 云智慧已开源集轻量级、聚合型、智能运维为一体的综合运维管理平台OMP(Operation Management Platform) ,具备 纳管、部署、监控、巡检、自愈、备份、恢复 等功能,可为用户提供便捷的运维能力和业务管理,在提高运维人员等工作效率的同时,极大提升了业务的连续性和安全性。点击下方地址链接,欢迎大家给OMP点赞送star,了解更多相关内容~ GitHub地址:https://github.com/CloudWise-OpenSource/OMP Gitee地址:https://gitee.com/CloudWise/OMP 微信扫描识别下方二维码,备注【OMP】加入AIOps社区运维管理平台OMP开发者交流群,与OMP项目PMC面对面交流,和更多行业大佬一起学习~

优秀的个人博客,低调大师

高效动画实现原理-Jetpack Compose 初探索

一、简介 Jetpack Compose是Google推出的用于构建原生界面的新Android 工具包,它可简化并加快 Android上的界面开发。Jetpack Compose是一个声明式的UI框架,随着该框架的推出,标志着Android 开始全面拥抱声明式UI开发。Jetpack Compose存在很多优点:代码更加简洁直观、应用开发效率显著提升、Kotlin API功能直观、预览工具强大等。 二、开发环境 为了获得更好的开发体验,笔者这里使用的是Android Studio Canary版本,这样可以无需配置一些设置和依赖。(下载地址) 打开工程,新建Empty Compose activity 模版,需要注意的是根目录下的build.gradle,相关的依赖com.android.tools.build和org.jetbrains.kotlin版本需要对应,否则可能出现出错的情形,这里使用的是: dependencies { classpath "com.android.tools.build:gradle:7.0.0-alpha15" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.30" } 这样就完成了项目的新建。 三、Jetpack Compose动画 Jetpack Compose提供了一些功能强大且可扩展的 API,可用于在应用界面中轻松实现各种动画效果。下文将会对Jetpack Compose Animations的常用方法进行介绍。 3.1 状态驱动动画:State Jetpack Compose动画是通过对状态的监听,即监听状态值的变化,使UI能实现自动更新。可组合函数可以使用 remember或者 mutableStateOf监听状态值的变化。如果状态值是不变的,remember函数会在每次重新组合中保持该值;如果状态是可变的,它会在值发生变化的时候触发重组,mutableStateOf将得到一个MutableState对象,它是一个可观察类型。 这种重组是创建状态驱动动画的关键。利用重组,它们会在可组合组件的状态发生任何变化时被触发。Compose动画是由State驱动的,动画相关的API也较容易上手,能比较容易创造出漂亮的声明式动画。 3.2 可见性动画: AnimatedVisibility 首先看下函数定义: @ExperimentalAnimationApi @Composable fun AnimatedVisibility( visible: Boolean, modifier: Modifier = Modifier, enter: EnterTransition = fadeIn() + expandIn(), exit: ExitTransition = shrinkOut() + fadeOut(), initiallyVisible: Boolean = visible, content: @Composable () -> Unit ) { AnimatedVisibilityImpl(visible, modifier, enter, exit, initiallyVisible, content) } 可以看出默认的动画是淡入放大、淡出收缩,实际中通过传入不同函数实现各种动效。 随着可见值的变化,AnimatedVisibility可为其内容的出现和消失设置动画。如下代码,可以通过点击Button,控制图片的出现和消失。 @Composable fun AinmationDemo() { //AnimatedVisibility 可见动画 var visible by remember { mutableStateOf(true) } Column( Modifier .fillMaxWidth() .fillMaxHeight(), Arrangement.Top, Alignment.CenterHorizontally ) { Button( onClick = { visible = !visible } ) { Text(text = if (visible) "Hide" else "Show") } Spacer(Modifier.height(16.dp)) AnimatedVisibility( visible = visible, enter = slideInVertically() + fadeIn(), exit = slideOutVertically() + fadeOut() ) { Image( painter = painterResource(id = R.drawable.pikaqiu), contentDescription = null, Modifier.fillMaxSize() ) } } } 通过监听visible的变化,可实现图片的可见性动画,效果如小图所示; 3.3 布局大小动画:AnimateContentSize 先看下函数的定义: fun Modifier.animateContentSize( animationSpec: FiniteAnimationSpec<IntSize> = spring(), finishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)? = null ) 可以为布局大小动画设置动画速度和监听值。 由函数的定义可以看出这个函数本质上就Modefier的一个扩展函数。可以通过变量size监听状态变化实现布局大小的动画效果,代码如下: //放大缩小动画 animateContentSize var size by remember { mutableStateOf(Size(300F, 300F)) } Column( Modifier .fillMaxWidth() .fillMaxHeight(), Arrangement.Top, Alignment.CenterHorizontally ) { Spacer(Modifier.height(16.dp)) Button( onClick = { size = if (size.height == 300F) { Size(500F, 500F) } else { Size(300F, 300F) } } ) { Text(if (size.height == 300F) "Shrink" else "Expand") } Spacer(Modifier.height(16.dp)) Box( Modifier .animateContentSize() ) { Image( painter = painterResource(id = R.drawable.pikaqiu), contentDescription = null, Modifier .animateContentSize() .size(size = size.height.dp) ) } } //放大缩小动画 animateContentSize var size by remember { mutableStateOf(Size(300F, 300F)) }​ Column( Modifier .fillMaxWidth() .fillMaxHeight(), Arrangement.Top, Alignment.CenterHorizontally ) { Spacer(Modifier.height(16.dp))​ Button( onClick = { size = if (size.height == 300F) { Size(500F, 500F) } else { Size(300F, 300F) } } ) { Text(if (size.height == 300F) "Shrink" else "Expand") } Spacer(Modifier.height(16.dp))​ Box( Modifier .animateContentSize() ) { Image( painter = painterResource(id = R.drawable.pikaqiu), contentDescription = null, Modifier .animateContentSize() .size(size = size.height.dp) ) }} 通过Button的点击,监听size值的变化,利用animateContentSize()实现动画效果,具体动效如下图所示: 3.4布局切换动画: Crossfade Crossfade可以通过监听状态值的变化,使用淡入淡出的动画在两个布局之间添加动画效果,函数自身就是一个Composable,代码如下: //Crossfade 淡入淡出动画 var fadeStatus by remember { mutableStateOf(true) } Column( Modifier .fillMaxWidth() .fillMaxHeight(), Arrangement.Top, Alignment.CenterHorizontally ) { Button( onClick = { fadeStatus = !fadeStatus } ) { Text(text = if (fadeStatus) "Fade In" else "Fade Out") } Spacer(Modifier.height(16.dp)) Crossfade(targetState = fadeStatus, animationSpec = tween(3000)) { screen -> when (screen) { true -> Image( painter = painterResource(id = R.drawable.pikaqiu), contentDescription = null, Modifier .animateContentSize() .size(300.dp) ) false -> Image( painter = painterResource(id = R.drawable.pikaqiu2), contentDescription = null, Modifier .animateContentSize() .size(300.dp) ) } } } 同样通过监听fadeStatus的值,实现布局切换的动画,具体的动效如图所示: 3.5单个值动画:animate*AsState 为单个值添加动画效果。只需提供结束值(或目标值),该 API 就会从当前值开始向指定值播放动画。 Jetpack Compose 提供了很多内置函数,可以为不同类型的数据制作动画,例如:animateColorAsState、animateDpAsState、animateOffsetAsState等,这里将介绍下animateFooAsState的使用,代码如下: //animate*AsState 单个值添加动画 var transparent by remember { mutableStateOf(true) } val alpha: Float by animateFloatAsState(if (transparent) 1f else 0.5f) Column( Modifier .fillMaxWidth() .fillMaxHeight(), Arrangement.Top, Alignment.CenterHorizontally ) { Button( onClick = { transparent = !transparent } ) { Text(if (transparent) "Light" else "Dark") } Spacer(Modifier.height(16.dp)) Box { Image( painter = painterResource(id = R.drawable.pikaqiu), contentDescription = null, Modifier .animateContentSize() .graphicsLayer(alpha = alpha) .size(300.dp) ) } } 动画效果如下图所示: 3.6 组合动画:updateTransition Transition 可同时追踪一个或多个动画,并在多个状态之间同步这些动画。具体的代码如下: var imagePosition by remember { mutableStateOf(ImagePosition.TopLeft) } Column( Modifier .fillMaxWidth() .fillMaxHeight(), Arrangement.Top, Alignment.CenterHorizontally ) { Spacer(Modifier.height(16.dp)) val transition = updateTransition(targetState = imagePosition, label = "") val boxOffset by transition.animateOffset(label = "") { position -> when (position) { ImagePosition.TopLeft -> Offset(-60F, 0F) ImagePosition.BottomRight -> Offset(60F, 120F) ImagePosition.TopRight -> Offset(60F, 0F) ImagePosition.BottomLeft -> Offset(-60F, 120F) } } Button(onClick = { imagePosition = ChangePosition(imagePosition) }) { Text("Change position") } Box { Image( painter = painterResource(id = R.drawable.pikaqiu), contentDescription = null, Modifier .offset(boxOffset.x.dp, boxOffset.y.dp) .animateContentSize() .size(300.dp) ) } } 其中,ImagePosition、ChangePosition分别为定义的枚举类、自定义函数。 enum class ImagePosition { TopRight, TopLeft, BottomRight, BottomLeft } fun ChangePosition(position: ImagePosition) = when (position) { ImagePosition.TopLeft -> ImagePosition.BottomRight ImagePosition.BottomRight -> ImagePosition.TopRight ImagePosition.TopRight -> ImagePosition.BottomLeft ImagePosition.BottomLeft -> ImagePosition.TopLeft } 动画的如下图所示: 四、结语 Jetpack Compose 已将动画简化到只需在我们的可组合函数中创建声明性代码的程度,只需编写希望 UI 动画的方式,其余部分由 Compose 管理。最后,这也是是 Jetpack Compose 的主要目标:创建一个声明式 UI 工具包来加速应用程序开发并提高代码可读性和逻辑性。 Jetpack Compose提供的声明式UI工具包,能做到使用更少的代码实现更多的功能,且代码的可读性和逻辑性也大大提高了。 作者:vivo互联网游戏客户端团队-Ke Jie

优秀的个人博客,低调大师

Android系统Bitmap内存分配原理与优化

# 一、前言 笔者最近致力于vivo游戏中心稳定性维护,在分析线上异常时,发现有相当一部分是由OutOfMemory引起。谈及OOM,我们一般都会想到内存泄漏,其实,往往还有另外一个因素——图片,如果对图片使用不当的话,很容易吃掉大量内存,从而导致异常。 尤其是游戏中心在2020末~2021初的几个重要版本,上线了很多内容相关的feature,引入大量图片、视频列表,从而导致线上OOM占比上升。 在这篇文章中,笔者将讲解一张看似普通的Bitmap对内存的占用,介绍Android Studio中帮助我们分析图片占用内存的工具,举例说明流行的两大图片加载框架:Glide、Picasso在加载图片时使用内存的不同方式,接着分析不同drawable目录下图片的显示策略,最后基于手机内存、版本,提出一种优化内存分配的方案。 # 二、查看图片内存占用 一张图片在内存占用的空间究竟有多少,普遍存在的一个误解是,图片本身在磁盘上/从网络下载下来是多大,就会占用多少的内存。这种说法是不正确的,图片占用内存的大小不取决于它本身的大小,而取决于图片库所采用的展示方式所申请的内存。 拿钢铁侠这张图片举例,它的尺寸是350*350,可以看到在电脑磁盘上,它只占36KB的空间。 ![](https://static001.geekbang.org/infoq/14/14e57769cd2c3c187bf635a6362924fa.jpeg) 我们创建一个简单的Demo,页面正中央是一个ImageView,用于显示这张钢铁侠图片。 ![](https://static001.geekbang.org/infoq/d7/d71766df61c3579ca07e7ed0a6fc9092.jpeg) 通过Android Studio进行heap dump,从而看图片所占用的内存。首先我们将显示图片时的内存快照保存下来。操作路径为Profiler->Memory->Heap Dump,这会生成一个dump文件,在其中可以看到当前堆的使用情况。 ![](https://static001.geekbang.org/infoq/01/013ea1970c9f032d18b3ed37bb7a2aa4.jpeg) 在下面这张图可以看到,程序运行时,“钢铁侠”这张图片占用的内存(Retained Size)是2560000bytes,约等于2.4MB内存。与它在磁盘上36KB的大小,相差了整整70倍! ![](https://static001.geekbang.org/infoq/57/577d2cc5b30edec69e28505ebaaa32d8.jpeg) **小技巧:如何查看dump文件中的图片** 在调试时,如果我们手头只有一个dump文件,往往需要还原图片内容,以帮助定位问题。有两种方式可以从dump文件里提取原图片。 **方式一:通过Android Studio直接查看** 如果dump文件来源自Android版本为7.1.1(Android N,API=25)及以下的设备,可以使用这种方法。选中Bitmap对象,直接在窗口的Bitmap Preview中查看图片内容(如上图),非常方便。 **方式二:通过MAT+GIMP查看** ![](https://static001.geekbang.org/infoq/38/38756f0ec29f8c58b01cdea00c643f9a.png) 这种方法适用于全部Android版本的设备,首先用[MAT](https://www.eclipse.org/mat/)打开dump文件,有时会发生下图的错误: ![](https://static001.geekbang.org/infoq/55/55f9118ce801b541b483b304f970b1ba.jpeg) 原因是Android Studio的Profiler生成的dump文件不是标准格式,我们可以使用位于路径SDK/platform-tools/hprof-conv.exe的工具将其转换为标准格式,转换命令为: ```java hprof-conv.exe ``` 将转换后的dump文件通过MAT打开,在其中找到Bitmap对象的byte\[\]属性,将其复制为image01.data文件。 ![](https://static001.geekbang.org/infoq/89/8912505bad9928701b735ebe1f4a2a37.jpeg) > Tip: 可以看到这里image01.data文件的尺寸是2.44MB,也正是在运行时图片所占用的内存。 然后用[GIMP](https://www.gimp.org/downloads/)工具打开该文件,在格式那里选择RGBA(大部分Bitmap都使用这种格式),宽与高可以在MAT中看到,笔者这里是800 * 800。设置好格式和宽高后,就可以看到图片的真实面目了。 ![](https://static001.geekbang.org/infoq/32/327dece8e1c11f3bcfe1c39aa064f6b6.jpeg) # 二、图片内存占用计算公式 在上一章节我们知道一个通过网络下载的36KB图片,在被加载到内存中时,需要2.4MB的空间。接下来解释这其中的换算关系,让我们记住一个公式: > 图片占用内存 = 图片质量 \* 宽 \* 高 这里面有“图片质量”、“宽”、“高”三个因素,它涉及到图片加载框架的实现,不同的框架,对于这三者的默认取值是不一样的,我们以当前最流行的Picasso和Glide为例。 **Picasso** 在Picasso中,图片默认显示的宽高与原始图片宽高一致。仍然以这张钢铁侠为例,图片本身是350px * 350px,当我们把它加载到200px * 200px的ImageView当中时,占用空间是**0.49MB**。 ![](https://static001.geekbang.org/infoq/0d/0daf64a596ad8272c220fafbc615cd9a.png) 因此,在目标ImageView小于图片尺寸的情况下,好的做法是使用不超过ImageView尺寸的图片源,一方面可以缩短图片下载时间,另一方面有助于优化内存占用。 **Glide** Glide则采用截然不同的处理方式,它最终使用的宽高是目标ImageView的宽高。如果我们把同样一张图片加载到200px * 200px的ImageView中,占用空间只有0.16MB。 ![](https://static001.geekbang.org/infoq/ce/ce2ec93e22d583df696cb848a936f6e0.png) **使Picasso达到与Glide同样的效果** Picasso的设计者也发现了这一缺点,提供一系列方法用来调整最终加载出来的图片尺寸,其一就是fit(),通过这个方法可以达到与Glide同样的效果。 ```java Picasso().get().load(IMAGE_URL).fit().into(imageVIEW) ``` **相反场景:小图加载到大ImageView中** 通常为了提供更清晰的界面,防止图片拉伸后失真模糊,设计师提供的图片都是高分辨率的,我们所面临的场景是将大图加载到小ImageView中。但也不排除相反的可能:将小图加载到大ImageView里面。这时Glide默认采用的内存策略是存在不足的:它采用目标ImageView的尺寸作为最终的宽和高。 举例说明,当把350 * 350的钢铁侠图片加载到600 * 600的ImageView中时,占用的内存高达1.41MB。 > 600 * 600 * 4bytes = 1.41MB 有没有一种方法,可以兼顾原图片与目标ImageView不同的大小关系呢?——有的,这就是centerInside()。 ```java Glide.with(this).load(IMAGE_URL).centerInside().into(imageView) ``` 借助centerInside()方法,可以达到“在原图片和目标ImageView中取最小宽高作为最终加载图片的尺寸”这样的效果。 # 三、图片质量 什么是“图片质量”?简单说就是用多少字节来表示一个像素点的颜色,它的学名叫做“**位深度**”,在图片属性当中可以看到。 图片位深度通常有1位、8位、16位、24位、32位。 ![](https://static001.geekbang.org/infoq/fa/faed08045fe303bde4deb0ce4a639f73.jpeg) PNG格式有8位、24位、32位三种形式,其中8位PNG支持两种不同 的透明形式(索引透明和alpha透明),24位PNG不支持透明,32位 PNG 在24位基础上增加了8位透明通道,因此可展现256级透明程度。 ![](https://static001.geekbang.org/infoq/af/af4b3784b242d039d9b7389348e695f6.png) Glide和Picasso默认采用的图片质量都是ARGB_8888、也就是带透明度的32位深度,一个像素点需要占用4bytes的内存,这也解释了为什么上文中的计算都是采用宽_高_4bytes的公式。 > 注:v4开始,Glide将ARGB\_8888作为默认配置。在那之前它一直默认使用RGB\_565。 对客户端使用的大部分图片来说,32位深度、16位深度的显示质量是肉眼较难分辨的,但它们在占用内存上相差了整整一倍。因此,笔者建议在大部分场景下,使用**RGB_565**作为加载图片的模式。以下两种场景除外: ![](https://static001.geekbang.org/infoq/08/0843f986c744659cf15e25380fa14eda.png) 1)含透明部分的图片:如果采用RGB_565图片格式来显示图片,是无法正常展现透明区域的。比如上方这个钢铁侠图片,原本透明的部分会被显示为黑色。 2)含渐变色并且对显示质量要求高的图片:32位比16位可以支持更多的颜色,在渐变的显示上呈现更加自然的过渡(如下图)。这时我们应当在显示质量和应用性能之间作取舍。对于低端设备,应用的稳定性比显示质量更加重要,笔者强烈建议采用16位深度来显示。 ![](https://static001.geekbang.org/infoq/2e/2e87ae0efc888a84159d6ca327ec0187.png) # 四、drawable目录下图片加载方式 项目的资源目录下,一般都有drawable-mdpi、drawable-hdpi、drawable-xhdpi、drawable-xxhdpi、drawable-xxxhdpi目录,它们是用来匹配不同显示密度的设备的,对应表格如下。 ![](https://static001.geekbang.org/infoq/c6/c6406aea63a02fe96cb71cdc20bf9570.png) 通过adb shell wm density可以获取当前设备的dpi,对Nexus 6P模拟器执行后,可以读取到它的dpi是560,属于xxxhdpi。 ```java $ adb shell wm densityPhysical density: 560 ``` 那么同一个图片放在不同目录下,对分配内存是否有影响呢?答案是有的,基于两步简单的推导: - 图片所在资源目录、设备密度两者决定图片最终显示在屏幕上的像素尺寸; - 像素尺寸、图片质量共同决定分配内存。 其中第2点已经在上文讲解过,这里主要分析第1点。使用图片编辑软件,将原本是350 * 350的钢铁侠图片放大至700 * 700,并分别放入xhdpi、xxxhdpi两个目录下。 为什么使用这样的组合呢?因为从上表得知,xhdpi与xxxhdpi的显示密度是1:2,意味着一台xxxhdpi的设备在显示drawable-xhdpi目录下的图片时,会将其放大为2倍进行展示。因此我们将350 * 350的骨片放入drawable-xhdpi,将700 * 700的图片放入drawable-xxxhdpi,预期它们最终在屏幕上显示的尺寸相同。 ![](https://static001.geekbang.org/infoq/48/4881926a543949d5906f23683b81cbe9.png) 在布局里创建两个ImageView,观察这两张图片最终的显示效果,以及分配内存情况。 ```java ​ ​ ​ ``` 显示效果以及内存分配如下: ![](https://static001.geekbang.org/infoq/e3/e3b699cb2b2ddb12dc0c7b08dcfbd2a2.jpeg) 可以分析得出以下结论: > 对于显示尺寸613 * 613的图片,其占据内存为613 * 613 * 4 = 1,503,076B ≈ 1.5MB,符合上文中我们对图片内存的分析; > > 决定图片占用内存的是其最终显示在屏幕上的尺寸,与图片本身分辨率、在哪个drawable目录下没有直接关系; > > 由于xxxhdpi密度是xhdpi密度的两倍,故在屏幕密度属于xxxhdpi的Nexus 6P设备上,drawable-xxxhdpi目录下的图片被以近似于原像素尺寸(700px)进行显示(显示为613px),而位于drawable-xhdpi目录下的图片被放大至2倍显示,最终显示尺寸同样是613px。 # 五、优化策略 在实际的开发中,我们希望中高端机型加载更清晰的图片(ARGB\_8888),以提升用户体验,对于低端机型则希望加载占用内存更小的图片(RGB\_565),以降低OOM发生的概率。可以在初始化Glide时进行这样的配置。需要留意的是不要对含透明区域的图片采用这种优化方案。 ```java @GlideModule class MyGlideModule : AppGlideModule() { ​ override fun applyOptions(context: Context, builder: GlideBuilder) { builder.setDefaultRequestOptions(RequestOptions().format(getBitmapQuality())) } ​ private fun getBitmapQuality(): DecodeFormat { return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || hasLowRam()) { // 低端机型采用RGB_565以节约内存 DecodeFormat.PREFER_RGB_565 } else { DecodeFormat.PREFER_ARGB_8888 } } } ``` # 六、小结 借助一些开源工具,我们可以便捷地定位大图,如滴滴开源的[DoKit](https://github.com/didi/DoraemonKit),篇幅原因不进行详细介绍。最后,对于我们日常开发总结几点建议,希望大家的应用稳定性节节攀升。 - 在多图的场景(比如RecyclerView)注意及时释放图片资源; - 使用占据内存更小的图片格式; - 图片源文件尺寸应当与目标ImageView相近; - 优先满足xxhdpi、xxxhdpi的图片资源需求; - 根据设备性能,采用不同的图片加载策略。 > 作者:vivo互联网客户端团队-Li Lei

优秀的个人博客,低调大师

Java进阶 | 泛型机制与反射原理

# 一、泛型的概念 ## 1、基础案例 泛型在Java中的应用非常广泛,最常见则是在集合容器中,先看下基础用法: ```java public class Generic01 { public static void main(String[] args) { Map map = new HashMap<>() ; map.put(88,"hello") ; // map.put("99","world") ; 输入编译错误 String value = map.get(88) ; // Integer value = map.get(88) ; 输出编译错误 System.out.println("value:"+value); } } ``` 声明一个map类型的容器,并且明确限定key和value的类型:分别为Integer,String,这样显然不能体现特别之处,可以对比下面的用法: ```java Map newMap = new HashMap() ; newMap.put("hello","world"); newMap.put(88,99); ``` 在不指定类型的情况下,键值对都默认为Object类型,这样的容器在使用的时候要时刻注意不同的key类型和取出的value值类型,并且value要做类型转换,相比之下泛型机制就很有必要。 可以看下Map接口的定义: ```java public interface Map { V get(Object key); V put(K key, V value); } ``` 在Map接口中,` `显然没有指定明确的类型,只是起到类型传递的作用,即K是key的类型,V是value的类型,在上面的源码中描述的很清楚,结合上面案例,在Map对象声明的时候可以明确指定` `的类型,也可以缺省为Object类型。 ## 2、泛型描述 泛型即可以理解为把数据类型作为参数,即参数化类型,用来提高代码的安全性,灵活性,避免类型转换;代码简洁明了,同时对于程序的可扩展性起到至关重要的作用。 首先设计一个简单的顶层接口,只定义一个callBack方法,和对出入参数的简单逻辑设定,这种设计在Java的源码设计中随处可见,例如上面的集合体系: ```java /** * 基础接口设计 * @param 返参类型 * @param 入参类型 */ interface CallBack { /** * 回调方法:V 方法入参 ,R 方法返回值 */ R callBack (V v) ; } ``` 为了实现具体的业务,再基于顶层接口向下做扩展,这里声明两个扩展接口,作为具体业务类的接口: ```java /** * 扩展接口设计01 */ interface ExtCallBack01 > extends CallBack > { @Override Integer callBack (List list) ; } /** * 扩展接口设计01 */ interface ExtCallBack02 > extends CallBack > { @Override Boolean callBack (Map map) ; } ``` 这样可以通过扩展接口去设计具体的业务类,提高程序的灵活可扩展性: ```java public class Generic02 { public static void main(String[] args) { new ExtCallBack01 >(){ @Override public Integer callBack(List list) { list.add("hello"); return list.size(); } }; new ExtCallBack02 > (){ @Override public Boolean callBack(Map map) { map.put("Fire",119L) ; return map.size()>0 ; } } ; } } ``` 通过上面这个案例,可以清楚的感觉到泛型机制的灵活和强大。 ## 3、泛型本质 泛型虽然可以使用在类,接口,方法,参数等各个地方,但是其约束能力是在代码的编译期: ```java public class Generic03 { public static void main(String[] args) { DefEntry defEntry1 = new DefEntry<>("hello") ; DefEntry defEntry2 = new DefEntry<>(999L) ; // Always True System.out.println(defEntry1.getClass()==defEntry2.getClass()); } } class DefEntry { private T param ; public DefEntry (T param){ this.param = param ; } } ``` 编译过程中,会对泛型合法性作校验,校验成功编译的class文件没有泛型信息,即泛型擦除掉,通过一个简单的命令查看编译后的文件: ``` javap -v Generic03.class ``` ![011.jpeg](https://s2.51cto.com/images/20210622/1624373751356679.jpeg?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 当然这也会带来安全问题: ```java public static void main(String[] args) throws Exception { Map map = new HashMap<>(); Method method = HashMap.class.getDeclaredMethod("put", new Class[] { Object.class, Object.class }); method.invoke(map,888L, 999L); // {888=999} System.out.println(map); // java.lang.ClassCastException System.out.println(map.get(888L)); } ``` 这里即通过反射的机制,绕开泛型,在map中放入程序语义上的非法值类型,在运行过程中获取值的时候才抛出类型转换异常。 # 二、反射机制 ## 1、基础描述 反射机制可以在程序运行时获取类的完整结构信息,并且可以动态的操作属性和方法等。 ![012.png](https://s2.51cto.com/images/20210622/1624373752948917.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 对于反射机制的理解,必须要对类编译和JVM加载,运行时数据区有清楚的认识,这块内容可以移步JVM系列的文章。 通过运行时动态获取类的结构,然后动态的创建对象并操作属性和方法,这种方式在实际开发中并不多用,这样很明显会消耗JVM资源,并且会忽略一些封装导致安全问题,这在上面【1】中已经案例说明了。 ## 2、反射的类库 - java.lang.Class:Class类 - java.lang.reflect.Constructor:构造器 - java.lang.reflect.Field:属性 - java.lang.reflect.Method:方法 **API之Class对象** 获取目标类型的Class对象常见方式,通过Class对象再获取相关结构信息,从而操作或者访问: ```java public static void main(String[] args) throws Exception { // Class对象回去 User user1 = new User(1,"name01") ; Class userClass1 = user1.getClass() ; Class userClass2 = Class.forName("com.java.reflect.User"); Class userClass3 = User.class ; System.out.println(User.class.getName()); System.out.println("userClass1==userClass2?"+(userClass1==userClass2)); System.out.println("userClass2==userClass3?"+(userClass2==userClass3)); // 类型创建和判断 Object object = User.class.newInstance() ; System.out.println("类型:"+(object instanceof User)); System.out.println("类型:"+(userClass3.isInstance(user1))); } ``` 输出结果: ![013.png](https://s2.51cto.com/images/20210622/1624373751990049.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 这里有个注意点:通过Class对象的`newInstance()`方法,即基于User类的无参构造器,首先要求User类具有无参构造方法。 **API之Constructor构造器** Class对象读取构造方法,可以分别获得全部构造方法,不同修饰类型的构造方法,或者根据构造参数类型指定获取: ```java public static void main(String[] args) throws Exception { Class userClass = User.class ; // 读取公共构造方法 Constructor[] userConArr = userClass.getConstructors(); printCon(userConArr); // 读取指定私有构造方法 Constructor privateCon = userClass.getDeclaredConstructor(Integer.class); System.out.println(privateCon); // 读取全部构造方法 userConArr = userClass.getDeclaredConstructors(); printCon(userConArr); // 调用公共构造方法创建对象 Constructor pubCon = userClass.getConstructor(Integer.class,String.class); Object pubUser = pubCon.newInstance(1,"hello") ; // 调用私有构造方法创建对象 Constructor priCon = userClass.getDeclaredConstructor(Integer.class); // 忽略private权限修饰符 priCon.setAccessible(Boolean.TRUE); Object priUser = priCon.newInstance(2) ; System.out.println(pubUser+"\n"+priUser); } public static void printCon (Constructor[] constructors){ for (Constructor constructor:constructors){ System.out.println(constructor); } } ``` 这里需要注意的是,通过调用`setAccessible(Boolean.TRUE)`方法,可以基于私有构造方法创建对象,这里明显违背了Java的基本设计原则,破坏代码的安全性。 ![014.jpeg](https://s2.51cto.com/images/20210622/1624373752302390.jpeg?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) **API之Field属性** Field保证成员变量的属性,修饰符,值管理等相关操作: ```java public static void main(String[] args) throws Exception { Class userClass = User.class ; // 获取公共字段 Field[] pubArr = userClass.getFields() ; printField(pubArr); // 获取全部字段 Field[] fieldArr = userClass.getDeclaredFields() ; printField(fieldArr); // 获取指定字段 Field emailField = userClass.getField("email") ; Field nameField = userClass.getDeclaredField("name") ; printField(new Field[]{emailField,nameField}); // 创建对象并操作属性 Object userObj = userClass.newInstance() ; nameField.setAccessible(Boolean.TRUE); nameField.set(userObj,"world"); emailField.set(userObj,"test@email.com"); System.out.println("userObj:"+userObj); } /** * 打印成员变量信息 */ public static void printField (Field[] fields){ for (Field field : fields){ System.out.println("声明:"+field); UserAnno userAnno = field.getAnnotation(UserAnno.class) ; System.out.println("注解:"+userAnno.desc()); String fieldName = field.getName() ; System.out.println("名称:"+fieldName); Type type = field.getGenericType() ; System.out.println("类型:"+type); } } ``` 注意这里获取Type类型信息,在有些特定的业务场景下还是十分有用的。 ![015.jpeg](https://s2.51cto.com/images/20210622/1624373752916506.jpeg?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) **API之Method方法** ```java public static void main(String[] args) throws Exception { Class userClass = User.class ; // 获取所有公共方法[包括父类和Object类方法] Method[] pubMethods = userClass.getMethods() ; printMethod(pubMethods); // 获取全部方法 Method[] allMethods = userClass.getDeclaredMethods() ; printMethod(allMethods); // 获取指定方法 Method method = userClass.getMethod("parName",String.class) ; printMethod(new Method[]{method}); // 调用方法 Object userObj = userClass.newInstance() ; Method setId = userClass.getDeclaredMethod("setId", Integer.class); setId.invoke(userObj,99) ; Method setName = userClass.getDeclaredMethod("setName", String.class); setName.invoke(userObj,"java") ; Method sayHi = userClass.getDeclaredMethod("sayHi", String.class); sayHi.setAccessible(Boolean.TRUE); sayHi.invoke(userObj,"c++"); System.out.println(userObj); } /** * 打印方法信息 */ public static void printMethod (Method[] methods){ for (Method method : methods){ System.out.println("定义:"+method); System.out.println("命名:"+method.getName()); UserAnno userAnno = method.getAnnotation(UserAnno.class) ; if (userAnno != null){ System.out.println("注解:"+userAnno.desc()); } Type[] paramTypeArr = method.getParameterTypes(); for (int i=0 ; i< paramTypeArr.length; i++){ System.out.print("参数"+(i+1)+"类型:"+paramTypeArr[i]+" ; "); } System.out.println("参数个数:"+method.getParameterCount()); } } ``` 注意这里对方法的获取远远不止类本身定义的,包括从父类继承的,和Java基础Object类中的。 ![016.jpeg](https://s2.51cto.com/images/20210622/1624373752194319.jpeg?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) # 三、源代码地址 ``` GitHub·地址 https://github.com/cicadasmile/java-base-parent GitEE·地址 https://gitee.com/cicadasmile/java-base-parent ``` ![](https://s4.51cto.com/images/blog/202008/11/7fd614ba86c567d35245a247b675bb35.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) **阅读标签** 【[Java基础](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzU4Njg0MzYwNw==&action=getalbum&album_id=1342230680016683009#wechat_redirect)】【[设计模式](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzU4Njg0MzYwNw==&action=getalbum&album_id=1709518416274833422#wechat_redirect)】【[结构与算法](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzU4Njg0MzYwNw==&action=getalbum&album_id=1709518416274833422#wechat_redirect)】【[Linux系统](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzU4Njg0MzYwNw==&action=getalbum&album_id=1334314473573744641#wechat_redirect)】【[数据库](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzU4Njg0MzYwNw==&action=getalbum&album_id=1376212870744358913#wechat_redirect)】 【[分布式架构](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzU4Njg0MzYwNw==&action=getalbum&album_id=1327025063014596608#wechat_redirect)】【[微服务](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzU4Njg0MzYwNw==&action=getalbum&album_id=1460269376221200386#wechat_redirect)】【[大数据组件](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzU4Njg0MzYwNw==&action=getalbum&album_id=1701021199339667459#wechat_redirect)】【[SpringBoot进阶](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzU4Njg0MzYwNw==&action=getalbum&album_id=1425309486268661760#wechat_redirect)】【[Spring&Boot基础](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzU4Njg0MzYwNw==&action=getalbum&album_id=1461797173297135618#wechat_redirect)】 【[数据分析](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzU4Njg0MzYwNw==&action=getalbum&album_id=1695231212027428866#wechat_redirect)】【[技术导图](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzU4Njg0MzYwNw==&action=getalbum&album_id=1506615482391511042#wechat_redirect)】【 [职场](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzU4Njg0MzYwNw==&action=getalbum&album_id=1719834087936278530#wechat_redirect)】

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

用户登录
用户注册