首页> 搜索结果页
"android gridview 加载网络图片" 检索
共 107 条结果
Android开源库,您懂多少?
前言:转眼间来到2017年8月份了,差不多又过去一年了,一年了我感觉我还是原地踏步,无论是项目经验还是底层的知识都很稀薄,在这个知识不断更新的时代,我也不得不一直保持学习的姿态,虽然一直在奔跑,但我乐意,我不觉得累!好吧,Android开源库,您懂多少?来一个总结归纳吧,我会不断学习与更新。github地址(持续更新欢迎star):https://github.com/zsml2016/GitHubNotes一、完整项目:1.   Meizhi:gank.iounofficial client, RxJava & Retrofit. (The project is no longer activelymaintained, we have a better project:2. ColorfulNews:A news-reading App (MVP+Dagger2+RxJava+Retrofit2+Material Design)。3.   Douya:开源的Material Design豆瓣客户端(A Material Design appfor douban.com)。4.   AndroidFire:一款新闻阅读 App框架,基于 Material Design + MVP+ RxJava + Retrofit + Glide,基本涵盖了当前android端开发最常用的主流框架,基于此框架可以快速开发一个app。5.   WechatLuckyMoney微信红包插件。6.   bilibili-android-client:An unofficial bilibili client for android7.   Twobbble:这是一个使用Kotlin开发的Dribbble客户端8.   BookReader:"任阅"网络小说阅读器,实现追书推荐收藏、书籍/标签检索、模拟翻书的翻页效果、缓存书籍、日夜间模式、书签、txt/pdf/epub书籍阅读、字体/主题/亮度设置、Wifi传书等功能~9.   TodayNews:一个仿今日头条的开源项目.10.   GithubApp:A Github Client App with MVP architecture useDagger2, RxJava, Retrofit, Okhttp 。11. SimpleNews:基于Material Design和MVP的新闻客户端。 二、网络请求: NoHttp:Android实现Http标准协议框架,支持缓存(提供五种缓存模式)、代理、重定向,底层可动态切换OkHttp、URLConnection。2. retrofit:Type-safeHTTP client for Android and Java by Square, Inc. 3.okhttp-OkGo:OkGo - 3.0 震撼来袭,该库是基于Http协议,封装了 OkHttp的网络请求框架,比 Retrofit更简单易用,支持RxJava,RxJava2,支持自定义缓存,支持批量断点下载管理和批量上传管理功能。4. okhttputils:okhttp的辅助类5. xUtils3:androidorm, bitmap, http, view inject...6. okhttp:AnHTTP+HTTP/2 client for Android and java applications.三、图片加载:1.   transferee:一个帮助您完成从缩略图到原图无缝过渡转变的神奇组件。2.   fresco:AnAndroid library for managing images and the memory they use. 3.   picasso:Apowerful image downloading and caching library for Android 。4.   glide:Animage loading and caching library for Android focused on smooth scrolling.5.   glide-transformations:An Android transformation library providing avariety of image transformations for Glide.6.   Android-Universal-Image-Loader:Powerful and flexible library for loading,caching and displaying images on Android.四.下拉刷新: SmartRefreshLayout:下拉刷新、上拉加载、RefreshLayout、OverScroll,Android智能下拉刷新框架,支持越界回弹,具有极强的扩展性,集成了几十种炫酷的Header和Footer。 2. XRefreshView:一个万能的android下拉上拉刷新的框架,完美支持recyclerview。3.LRecyclerView:RecyclerView下拉刷新,自动加载更多;仿iOS侧滑Item删除菜单。4. BreakOutToRefresh:Play BreakOut while loading - A playable pullto refresh view using SpriteKit。5.FunGameRefresh :好玩的下拉刷新控件,让我们一起来回味童年。6. CircleRefreshLayout:a custom pull-to-refresh layout which containsa interesting animation。7.FlyRefresh:The Android implementation of Replace, designed by Zee Youn. I implementthis as a FlyRefresh layout. The content of the layout can be anyNestedScrollingChild, such as a RecyclerView, NestedScrollView,VerticalGridView, etc. This library can also work with NestedScrollingParent asparent, such as CoordinatorLayout.8. Phoenix Pull-to-Refresh9.TwinklingRefreshLayout:RefreshLayout that support for OverScroll andbetter than ios.支持下拉刷新和上拉加载的RefreshLayout,自带越界回弹效果,支持RecyclerView,AbsListView,ScrollView,WebView。10. XRecyclerView:一个RecyclerView实现pullrefresh和loadmore featrues.you可以像标准RecyclerView一样使用它。11. RecyclerRefreshLayout:A pull to refresh layout for android, theRecyclerRefreshLayout is based on the SwipeRefreshLayout. support all theviews, highly customizable, code simplicity, etc12.BGARefreshLayout-Android:多种下拉刷新效果、上拉加载更多、可配置自定义头部广告位。13. Android-PullToRefresh五.数据解析:1. fastjson:Afast JSON parser/generator for Java .2.   gson六.依赖注解:1.   butterknife:Bind Android views and callbacks to fields and methods.七.数据存储: afinal: Afinal是一个android的ioc,orm框架,内置了四大模块功能:FinalAcitivity,FinalBitmap,FinalDb,FinalHttp。通过finalActivity,我们可以通过注解的方式进行绑定ui和事件。通过finalBitmap,我们可以方便的加载bitmap图片,而无需考虑oom等问题。通过finalDB模块,我们一行代码就可以对android的sqlite数据库进行增删改查。通过FinalHttp模块,我们可以以ajax形式请求http数据。详情请通过以下网址查看。2. LitePal:AnAndroid library that makes developers use SQLite database extremely easy.3. DiskLruCache:Java implementation of a Disk-based LRU cache which specifically targetsAndroid compatibility.4. greenDAO:greenDAO is a light & fast ORM solution forAndroid that maps objects to SQLite databases.  八.进度、加载等待、对话框:1、BubbleSeekBar:A beautiful Android custom seekbar, which has a bubble view withprogress appearing upon when seeking.自定义SeekBar,进度变化由可视化气泡样式呈现2、SVProgressHUD:SVProgressHUD For Android。3、SmoothProgressBar:A small Android library allowing you to have asmooth and customizable horizontal indeterminate ProgressBar。4、 WaveLoading:Awave-like loading drawable。5、CircleProgress:CircleProgress, DonutProgress, ArcProgress。6.NiftyDialogEffects:漂亮的模式对话框效果。7. sweet-alert-dialog:SweetAlert for Android, a beautiful and cleveralert dialog.8. android-adDialog:一个简单,强大的广告活动弹窗控件。 LoadingDrawable:Some beautiful android loading drawable, can be combined with any viewas the LoadingView or the ProgressBar. Besides, some Drawable can customize theloading progress too.15.  Android-SpinKit:Android loading animations。16. NumberProgressBar:A beautiful, slim Android ProgressBar.17. MaterialProgressBar:Material Design ProgressBar with consistentappearance。18. MaterialLoadingProgressBar:MaterialLoadingProgressBar provide a styledProgressBar which looks like SwipeRefreshLayout's loading indicator(support-v4v21+)。19. material-dialogs:A beautiful, fluid, and customizable dialogs API。20.spots-dialog:Android AlertDialog with moving dots progress indicator. 九.kotlin:1. kotlin:The Kotlin Programming Language.十.导航 指示器:1.ahbottomnavigation:A library to reproduce the behavior of theBottom Navigation guidelines from Material Design. NavigationTabBar:Navigation tab bar with colorful interactions.3. AlphaIndicatorView:仿微信底部tab标签,滑动的时候颜色渐变,使用极其简单,只需要两行代码。4. stepper-indicator:用于入职的步骤指示器或简单的查看器。5. CoordinatorTabLayout:Combination of TabLayout andCoordinatorLayout./TabLayout和CoordinatorLayout相结合的折叠控件。6. MagicIndicator:A powerful, customizable and extensible ViewPager indicator framework.As the best alternative of ViewPagerIndicator, TabLayout andPagerSlidingTabStrip ——强大、可定制、易扩展的ViewPager指示器框架。是ViewPagerIndicator、TabLayout、PagerSlidingTabStrip的最佳替代品。支持角标,更支持在非ViewPager场景下使用(使用hide()、show()切换Fragment或使用setVisibility切换FrameLayout里的View等)7. FlycoTabLayout:An Android TabLayout Lib。 十一.搜索框:1. PersistentSearch:A clone of the Google Now/Maps/Play persistent search bar。2. floatingsearchview:A search view that implements a floating searchbar also known as persistent search。3. IconCenterView:a iOS-Style search bar in Android。4. MaterialSearchBar:Material Design Search Bar for Android。5. SearchDialog:仿bilibili搜索框效果(三句代码实现)。十二.工具类:1. AndroidHttpCapture:AndroidHttpCapture网络诊断工具是一款Android手机抓包软件主要功能包括:手机端抓包、PING/DNS/TraceRoute诊断、抓包HAR数据上传分享。你也可以看成是Android版的"Fiddler" (^o^)/~。2. Condom:一个超轻超薄的Android工具库,阻止三方SDK中常见的有害行为,而不影响应用自身的功能。(例如严重影响用户体验的『链式唤醒』)。3. android-utils:It contains most of the Android utility classes. 4.AndroidDevTools: 收集整理Android开发所需的Android SDK、开发中用到的工具、Android开发教程、Android设计规范,免费的设计素材等。5. androidscreen:android screen是一个基于Java开发的工具,目的是帮助android开发者生成多屏幕适配的配置文件,减少开发难度。6.TinyPinyin:适用于Java和Android的快速、低内存占用的汉字转拼音库。7. packer-ng-plugin:下一代Android打包工具,100个渠道包只需要10秒钟 。8. walle:Android Signature V2 Scheme签名下的新一代渠道包打包神器。9.leakcanary: A memory leak detection library for Android andJava.10. AutoInstaller:应用自动静默更新安装库.11. AndroidUtilCode:Android developers should collect the following utils(updating)。12. baseAdapter:Android 万能的Adapter forListView,RecyclerView,GridView等,支持多种Item类型的情况。13. RxDownload:基于RxJava打造的下载工具,支持多线程下载和断点续传,智能判断是否支持断点续传等功能。 十三.广告轮播:1. banner:Android广告图片轮播控件,支持无限循环和多种主题,可以灵活设置轮播样式、动画、轮播和切换时间、位置、图片加载框架等!2. AndroidImageSlider:An amazing and convenient Android image slider. 十四.欢迎向导:1. XhsWelcomeAnim:小红书欢迎引导第二版。2. WoWoViewPager:结合ViewPager和动画来提供一种创建应用程序指南页面的简单方法。3. ShowcaseView:Highlight the best bits of your app to users quickly, simply, andcool...ly。4. MaterialShowcaseView:A Material Design themed ShowcaseView forAndroid。5. Highlight:一个用于app指向性功能高亮的库。6. GuideView:最最轻量级的新手引导库,能够快速为任何一个View创建一个遮罩层,支持单个页面,多个引导提示,支持为高亮区域设置不同的图形,支持引导动画,方便扩展,良好支持fragment。 十五.动画渲染:1. android-activityAnim:五种实现activity动画切换的方式。2. ParticleTextView:一个用粒子动画显示文字的 Android自定义 View。3. animate:Anapplication demoing meaningful motion on Android。4. AndroidViewAnimations:可爱视图动画收藏。5. LTMorphingLabel:Graceful morphing effects for UILabel written in Swift.6. HTextView:Animationeffects to text, not really textview。7. Material-Animations:Android Transition animations explanation withexamples.8. FloatingView:FloatingView can make the target view floating above the anchor viewwith cool animation.十六 . Android复习面试:1. android-interview-questions:一个老外总结的-Android的面试问题2. android-interview-questions-cn :由国内大神stormzhang组织的-最全面的高质量Android面试指南。3. ResumeSample:Resumetemplate for Chinese programmers .程序员简历模板系列。包括PHP程序员简历模板、iOS程序员简历模板、Android程序员简历模板、Web前端程序员简历模板、Java程序员简历模板、C/C++程序员简历模板、NodeJS程序员简历模板、架构师简历模板以及通用程序员简历模板。4. android-skill-summary:Android技能总结,各种基础和进阶内容的资料收集。5. LearningNotes:Enjoy Learning.比较系统的学习笔记。十七 . Android多媒体:1. VCameraDemo:微信小视频+秒拍,FFmpeg库封装。2. android-UniversalMusicPlayer:This sample showshow to implement an audio media app that works across multiple form factors andprovide a consistent user experience on Android phones, tablets, Auto, Wear andCast devices。3. PLDroidPlayer:PLDroidPlayer 是 Pili直播 SDK的安卓播放器。支持所有直播常用的格式,如:RTMP、HLS、FLV。拥有优秀的功能和特性,如:首屏秒开、追帧优化、丰富的数据和状态回调、硬解软解支持。而且可以根据自己的业务进行高度定制化开发。4. PLDroidShortVideo:PLDroidShortVideo是七牛推出的一款适用于 Android平台的短视频 SDK,提供了包括美颜、滤镜、水印、断点录制、分段回删、视频编辑、混音特效、本地/云端存储在内的多种功能,支持高度定制以及二次开发。5. video-live :视频直播。6. barcodescanner :适用于Android的条形码扫描程序库。7. BGAQRCode-Android :QRCode 扫描二维码、扫描条形码、相册获取图片后识别、生成带 Logo二维码、支持微博微信 QQ二维码扫描样式。8. GSYVideoPlayer :视频播放器(IJKplayer),HTTPS支持,支持弹幕,支持基本的拖动,声音、亮度调节,支持边播边缓存,支持视频本身自带rotation的旋转(90,270之类),重力旋转与手动旋转的同步支持,支持列表播放,直接添加控件为封面,列表全屏动画,视频加载速度,列表小窗口支持拖动,5.0的过场效果,调整比例,多分辨率切换,支持切换播放器,进度条小窗口预览,其他一些小动画效果,rtsp、concat、mpeg。9.ijkplayer :基于FFmpegn3.3的Android / iOS视频播放器,支持MediaCodec,VideoToolbox。10. VideoListPlayer :Playvideo in ListView or RecyclerView。11. JieCaoVideoPlayer :AndroidVideoPlayer MediaPlayer VideoView MediaView Float View And Fullscreen。12. UniversalVideoView :Abetter Android VideoView with more Media Controller customization.一个更好用的Android VideoView. 十八  . 选择器:1. MaterialDateTimePicker :在Android上选择日期或时间:2. DatePicker :Usefuland powerful date picker for android。3. WheelPicker :Simpleand fantastic wheel view in realistic effect for android.4. TakePhoto :一款用于在Android设备上获取照片(拍照或从相册、文件中选择)、裁剪图片、压缩图片的开源工具库。5. CharacterPickerView :可实现三级联动的选择器,高仿iOS的滚轮控件,字体大小自适应。6. android-zxingLibrary :几行代码快速集成二维码扫描功能7. AndroidPicker :安卓选择器类库,包括日期选择器、时间选择器、单项选择器、双项选择器、城市地址选择器、车牌号选择器、数字选择器、星座选择器、生肖选择器、颜色选择器、文件选择器、目录选择器等,可自定义顶部及底部界面,可自定义窗口动画……8. Android-FilePicker :Photopickerand document picker for android。9. SuspensionIndexBar :仿美团选择城市、微信通讯录、饿了么点餐列表的导航悬停分组索引列表。10. Android-PickerView :Thisis a picker view for android , support linkage effect, timepicker andoptionspicker.(时间选择器、省市区三级联动)。十九 . UI扩展:1.  AndroidSwipeLayout :The Most Powerful Swipe Layout!2. BezierMaker :通过de Casteljau算法绘制贝塞尔曲线,并计算它的切线,实现1-7阶贝塞尔曲线的形成动画。3. cropiwa :适用于Android的可配置自定义裁剪窗口小部件。4. SlideSwitch :A widget you can slide it to open or close something。5. ExpandableTextView :Android's TextView that can expand/collapselike the Google Play's app description。6. XCL-Charts :Android图表库(XCL-Charts is a freecharting library for Android platform.),基于AndroidCanvas来绘制各种图表,使用简便,定制灵活。目前支持3D/非3D/背向式/横向/竖向柱形图(BarChart)、3D/非3D饼图(PieChart)、堆叠图(Stacked Bar Chart)、面积图(Area Chart)、折线图(Line Chart)、曲线图(Spline Chart)、环形图(DountChart)、南丁格尔玫瑰图(RoseChart)、仪表盘(Dial Chart)、刻度盘(Gauge Chart)、雷达图(Radar Chart)、漏斗图(FunnelChart)、圆形图。7. GridPasswordView :An android password view that looks like the pay password view in wechatapp and alipay app.8. BiuEditText :biu,biu,一个有趣的EditText。9. android-card-slide-panel :使用户能够顺利,连续地向左或向右滑动卡片。10. Jgraph :一个视觉效果还不错的图表控件。11. SwipeStack :Asimple, customizable and easy to use swipeable view stack for Android.12. MultiImageSelector :仿微信实现多图选择。支持单选和多选两种模式13. SwitchButton :SwitchButton.Anbeautiful+lightweight+custom-style-easy switch widget for Android,minSdkVersion>= 1114. StickyHeaderListView :打造炫酷列表之StickyHeaderListView:标题渐变、吸附悬浮、筛选分类、动态头部等。15. PhotoView :图片浏览缩放控件。16. ImageSelector :Android图片选择器。充分自由定制,极大程度简化使用,支持图库多选/图片预览/单选/照片裁剪/拍照/自定义图片加载方式/自定义色调/沉浸式状态栏。17. HwTxtReader :轻量级Txt阅读器组件,支持windows下的常见几种编码格式,使用翻页阅读。18. PullZoomView :Android自定义ListView和ScrollView,可以放大。19. Space-Navigation-View :SpaceNavigation is a library allowing easily integrate fully customizable GoogleSpaces like navigation to your app.20. CoordinatorTabLayout :Combinationof TabLayout and CoordinatorLayout./TabLayout和CoordinatorLayout相结合的折叠控件。21. PopsTabView :PopsTabView是个filter容器,他可以快速,构建不同筛选样式,自由组合PopWindow成一组tab.22. android_radiogroup_MutilRadioGroup :androidmultiform RadioGroup:复杂样式的单选框,自定义RadioGroup实现radiobutton多行多列嵌套在各种布局中排列布局。23. SwipeCardView :一个带渐变层叠动画的左右滑动效果(类似于探探、tinder)。24. FlowLayout :Android流式布局,支持单选、多选等,适合用于产品标签等。25. BottomBar :Acustom view component that mimics the new Material Design Bottom Navigationpattern.26. ZLayoutManager :ome custom LayoutManager .Such as SwipeCard、FLowLayout。一些自定义的LayoutManager,仿探探、人人影视炫动滑动卡片层叠和流式布局等。27. BGAAdapter-Android : 在AdapterView和 RecyclerView中通用的 Adapter和ViewHolder。RecyclerView支持 DataBinding、多种 Item类型、添加 Header和 Footer。RecyclerView竖直方向通用分割线 BGADivider。28. BGABadgeView-Android :Android徽章控件。29. StepView :Step by step,justuse HorizontalStepView,VerticalStepView.step indicator,flow indicator,timeline,orderprocess,express status。30. FloatingActionButton :FloatingActionButton:AndroidFloating Action Button based on Material Design specification。31. FloatingActionButton :Android floating action button32. EasyRecyclerView :将开发中常用的RecyclerView的各种需求封装进库。提升开发效率。33. ScrollTextView :Android跑马灯式文字滚动。34. SwitchButton :Acute widget of Switch Button for you to create beautiful and friendly UI.35. androidWheelView :仿照iOS的滚轮控件,从请吃饭apk反编译出来的。36. NineGridView :类似QQ空间,微信朋友圈,微博主页等,展示图片的九宫格控件,自动根据图片的数量确定图片大小和控件大小,使用Adapter模式设置图片,对外提供接口回调,使用接口加载图片,支持任意的图片加载框架,如 Glide,ImageLoader,Fresco,xUtils3,Picasso 等,支持点击图片全屏预览大图。37. SwipeBack :SwipeBackis an android library that can finish a activity by using gesture.38. RecyclerViewCardGallery : RecyclerView实现Card Gallery效果,替代ViewPager方案。39. InfiniteCycleViewPager :Infinitecycle ViewPager with two-way orientation and interactive effect.。40. CountdownView :AndroidCountdown View。41. CircleImageView :Acircular ImageView for Android。42. SwipeDelMenuLayout :Themost simple SwipeMenu in the history, 0 coupling, support any ViewGroup. Stepintegration swipe (delete) menu, high imitation QQ, iOS. ~史上最简单侧滑菜单,0耦合,支持任意ViewGroup。一步集成侧滑(删除)菜单,高仿QQ、IOS。~43. PhotoView :Implementationof ImageView for Android that supports zooming, by various touch gestures.二十 . 热修复:1. tinker :Tinker是Android的热修复解决方案库,它支持dex,库和资源更新,无需重新安装apk。 二十一 . 科学上网:1.   lantern :蓝灯2. hosts:  最新可用的googlehosts文件。二十 二. 其他:1. JKeyboardPanelSwitch :Forresolve the layout conflict when keybord & panel are switching (Android键盘面板冲突布局闪动处理方案).2. MagicaSakura :MagicaSakura是 Android多主题框架。~ isan Android multi theme library which supporting both daily colorful theme andnight theme. 3.folding-cell-android :FoldingCellis a material design expanding content cell inspired by folding paper material。4. ILOVEYOU :青春总会因为一个人开始闪闪发亮!5. awesome-android-tips :somecode tips in android 。6. RxJavaSamples :RxJava和 Retrofit结合使用的几个最常见使用方式举例。7.android-slidingactivity :可让您从活动中向下滑动以关闭它。8. awesome-android-ui :精美的AndroidUI / UX库列表9.AndroidSdkSourceAnalysis :android sdk源码解析——旨在帮助Android开发者更好的学习Android!我们只是一群普通的程序员,但是,我们热爱分享,想热热闹闹的玩点有意义的事!如果你也想陪我们一起愉快的玩耍,欢迎加入我们!Issues认领分析文章!10. android-open-project :Acategorized collection of Android Open Source Projects。11. Android:GitHub上最火的Android开源项目,所有开源项目都有详细资料和配套视频。12.my-git:[](https://github.com/xirong/my-git)Individual collecting material of learning Git(有关 git的学习资料)。13. gitignore :Acollection of useful .gitignore templates。(有用的.git忽视模板的集合)14. Colorful :基于Theme的Android动态换肤库,无需重启Activity、无需自定义View,方便的实现日间、夜间模式。15. Android-Skin-Loader :一个通过动态加载本地皮肤包进行换肤的皮肤框架。16. DanmakuFlameMaster :Android开源弹幕引擎·烈焰弹幕使~17.  DylanStepCount :Android精准计步器。18. Android-Plugin-Framework:Android插件框架,免安装运行插件APK,支持独立插件和非独立插件。19.  shopcar:仿饿了么购物车。20. zxing:Official ZXing ("Zebra Crossing") project home。21. html-textview :TextViewto display simple HTML content。22. EventBus :Android optimized eventbus that simplifies communication between Activities, Fragments, Threads,Services, etc. Less code, better quality. 
文章
存储  ·  缓存  ·  Java  ·  程序员  ·  开发工具  ·  Android开发  ·  iOS开发  ·  git  ·  Kotlin  ·  数据库管理
2022-10-29
DiskCacheDir + LruCache + AsynTask 的封装,实现照片墙的效果
上次对 DiskLruCache 进行了封装,但是他只能每次加载一张图片,不能放在ListView 等控件中使用。下面进行一次二次封装。首先是网络请求public class NetRequest { private OkHttpClient client = new OkHttpClient(); public byte[] request(final String url) { Request request = new Request.Builder() .url(url) .get() .build(); try { final ResponseBody body = client.newCall(request).execute().body(); if (body != null) { return body.bytes(); } } catch (IOException e) { e.printStackTrace(); return null; } return null; }然后是封装的 DiskLruCache* @author Lv * Created at 2019/6/14 * * 图片缓存 */ @SuppressWarnings({"ConstantConditions", "ResultOfMethodCallIgnored"}) public class DiskBitmapCache { private DiskLruCache mDiskLruCache; private Handler mHandler = new Handler(Looper.myLooper()); public DiskBitmapCache(Context context, String uniqueName){ open(context,uniqueName); } public DiskBitmapCache(){} /** * 用于返回 硬盘缓存后的数据 */ public interface OnCacheDataListener { /** * 返回数据 * * @param bytes 缓存的数据 */ void onData(byte[] bytes); } /** * 打开磁盘缓存 * * @param context Context * @param uniqueName 缓存文件夹的名字 * @return 返回一个 boolean 类型,true 表示 创建成果 */ public boolean open(Context context, String uniqueName) { File cacheDir = getCacheDir(context, uniqueName); //路径是否存在,不存在则创建 if (!cacheDir.exists()) { cacheDir.mkdirs(); } try { // 1,数据的缓存地址,2,指定当前应用程序的版本号 // 3,指定同一个 key 可以对应多少个缓存文件,基本都是1 // 4,指定据图可以缓存多少字节的数据 mDiskLruCache = DiskLruCache.open(cacheDir, getVersion(context), 1, 10 * 1024 * 1024); return mDiskLruCache != null; } catch (IOException e) { e.printStackTrace(); } return false; } /** * 传入对应的 url ,进行缓存 * * @param url 需要缓存的图片 ,缓存的文件名字为 url * @param listener 回调,将数据进行缓存后,返回一份,如果不需要传入 null 即可。 */ public void writeData(String url, final OnCacheDataListener listener) { downloadData(url, listener); } /** * 传入指定的 key 和 bitmap ,对图片进行缓存 * * @param key 这个key 为缓存文件的名字 * @param bitmap 要缓存的图片 * @return 返回缓存的结果 */ public boolean writeData(String key, Bitmap bitmap) { key = hashKeyForDisk(key); DiskLruCache.Editor edit = null; try { edit = mDiskLruCache.edit(key); if (edit != null) { OutputStream outputStream = edit.newOutputStream(0); boolean compress = bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream); edit.commit(); return compress; } else { return false; } } catch (IOException e) { e.printStackTrace(); if (edit != null) { try { edit.abort(); } catch (IOException e1) { e1.printStackTrace(); } } return false; } } /** * 根据 传入的 key 来查找缓存 * * @param key 用来读取缓存的 key * @return 返回缓存的图片字节,没有则为 null */ public byte[] readCache(String key) { try { List<Byte> data = new ArrayList<>(); key = hashKeyForDisk(key); DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); if (snapShot != null) { InputStream is = snapShot.getInputStream(0); byte[] bytes = new byte[2048]; int len; while ((len = is.read(bytes)) != -1) { for (int i = 0; i < len; i++) { data.add(bytes[i]); } } bytes = new byte[data.size()]; for (int i = 0; i < bytes.length; i++) { bytes[i] = data.get(i); } return bytes; } else { return null; } } catch (IOException e) { e.printStackTrace(); } return null; } /** * 根据 key 删除指定的 缓存 * * @param key 缓存的 key * @return 成功则返回 true */ public boolean removeCache(String key) { String md5Key = hashKeyForDisk(key); boolean remove = false; try { remove = mDiskLruCache.remove(md5Key); return remove; } catch (IOException e) { e.printStackTrace(); } return remove; } /** * 判断 该key 是否由缓存的图片 * @param key 缓存文件对应的 key * @return 返回true 表示 有缓存,可以直接读取 */ public boolean isCache(String key) { byte[] bytes = readCache(key); return bytes != null; } /** * @return 返回 DiskLruCache 的实例 */ public DiskLruCache getInstance() { return mDiskLruCache; } /** * @return 返回缓存的大小,以字节为单位 */ public long size() { return mDiskLruCache.size(); } /** * 将内存中的操作记录同步到日志文件,这个方法非常重要 * 频繁地调用这个这个方法不会有任何好处,标准的做法是在 onPause 中调用一次就可以了 */ public void flush() { if (mDiskLruCache != null) { try { mDiskLruCache.flush(); } catch (IOException e) { e.printStackTrace(); } } } /** * 这个方法用于将DiskLruCache关闭掉,是和open()方法对应的一个方法。 * 关闭掉了之后就不能再调用DiskLruCache中任何操作缓存数据的方法, * 通常只应该在Activity的onDestroy()方法中去调用close()方法 */ public void close() { if (mDiskLruCache != null) { try { mDiskLruCache.close(); } catch (IOException e) { e.printStackTrace(); } } } /** * 这个方法用于将所有的缓存数据全部删除 */ public void delete() { try { mDiskLruCache.delete(); } catch (IOException e) { e.printStackTrace(); } } /** * @return 获取 缓存的路径 */ private File getCacheDir(Context context, String uniqueName) { String cachePath; // 判断 SD 卡是否可用 if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) { //获取 有sd 卡时的路径 cachePath = context.getExternalCacheDir().getPath(); } else { // 获取 无sd 卡时的路径 cachePath = context.getCacheDir().getPath(); } //File.separator 分隔符 / return new File(cachePath + File.separator + uniqueName); } private int getVersion(Context context) { try { PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); return info.versionCode; } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } return 1; } private String hashKeyForDisk(String key) { String cacheKey; try { final MessageDigest mDigest = MessageDigest.getInstance("MD5"); mDigest.update(key.getBytes()); cacheKey = bytesToHexString(mDigest.digest()); } catch (NoSuchAlgorithmException e) { cacheKey = String.valueOf(key.hashCode()); } return cacheKey; } private String bytesToHexString(byte[] digest) { StringBuilder sb = new StringBuilder(); for (byte b : digest) { String hex = Integer.toHexString(0xFF & b); if (hex.length() == 1) { sb.append('0'); } sb.append(hex); } return sb.toString(); } private void downloadData(final String url, final OnCacheDataListener listener) { try { String key = hashKeyForDisk(url); final DiskLruCache.Editor editor = mDiskLruCache.edit(key); final OutputStream ops = editor.newOutputStream(0); if (ops != null) { new Thread(new Runnable() { @Override public void run() { NetRequest okhttp = new NetRequest(); try { final byte[] result = okhttp.request(url); ops.write(result); editor.commit(); mHandler.post(new Runnable() { @Override public void run() { listener.onData(result); } }); } catch (IOException e) { e.printStackTrace(); try { if (editor != null) { editor.abort(); } } catch (IOException e1) { e1.printStackTrace(); } } } }).start(); } } catch (IOException e) { e.printStackTrace(); } } }上面两个 和上一篇博客内容是一样的,下面就看一下行加了什么LruCache 的封装/** * @author Lv * Created at 2019/6/16 */ @SuppressWarnings("WeakerAccess") public class LruCachePhoto { /** * 图片 缓存技术的核心类,用于缓存下载好的所有图片, * 在程序内存达到设定值后会将最少最近使用的图片移除掉 */ private LruCache<String, Bitmap> mMenoryCache; public LruCachePhoto() { //获取应用最大可用内存 int maxMemory = (int) Runtime.getRuntime().maxMemory(); //设置 缓存文件大小为 程序最大可用内存的 1/8 int cacheSize = maxMemory / 8; mMenoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap value) { return value.getByteCount(); } }; } /** * 从 LruCache 中获取一张图片,如果不存在 就返回 null * @param key LurCache 的键,这里是 图片的地址 * @return 返回对应的 Bitmap对象,找不到则为 null */ public Bitmap getBitmapFromMemoryCache(String key){ return mMenoryCache.get(key); } /** * 添加一张图片 * @param key key * @param bitmap bitmap */ public void addBitmapToCache(String key,Bitmap bitmap){ mMenoryCache.put(key,bitmap); } }AsynTask 的封装package com.admin.utill.net.cache; import android.annotation.SuppressLint; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.AsyncTask; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.widget.GridView; import android.widget.ImageView; import android.widget.ListView; import com.admin.utill.net.NetRequest; /** * @author Lv * Created at 2019/6/15 */ @SuppressLint("StaticFieldLeak") public class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> { private LruCachePhoto mCachePhoto; private GridView mGridView; private DiskBitmapCache mDataCache; private ListView mListView; private ImageView mImageView; private Object mTag; private static final String TAG = "BitmapWorkerTask"; /** * @param mCachePhoto 用于缓存下载好的图片,将图片缓存到内存 * @param gridView 需要显示图片的 控件 * @param Tag 每一个条目的 Tag */ public BitmapWorkerTask(LruCachePhoto mCachePhoto, DiskBitmapCache dataCache, GridView gridView, Object Tag) { this.mGridView = gridView; init(mCachePhoto, dataCache, Tag); } public BitmapWorkerTask(LruCachePhoto mCachePhoto, DiskBitmapCache dataCache, ListView listView, Object tag) { this.mListView = listView; init(mCachePhoto, dataCache, tag); } public BitmapWorkerTask(LruCachePhoto mCachePhoto, DiskBitmapCache dataCache, ImageView imageView) { init(mCachePhoto, dataCache, null); this.mImageView = imageView; } private void init(LruCachePhoto mCachePhoto, DiskBitmapCache dataCache, Object tag) { this.mCachePhoto = mCachePhoto; this.mDataCache = dataCache; this.mTag = tag; } @Override protected Bitmap doInBackground(String... strings) { String imageUlr = strings[0]; //获取内存的缓存 Bitmap bitmap = mCachePhoto.getBitmapFromMemoryCache(imageUlr); if (bitmap!=null){ return bitmap; } //判断本地是否有缓存 if (!mDataCache.isCache(imageUlr)) { //没有缓存 bitmap = downLoadBitmap(imageUlr); } else { byte[] bytes = mDataCache.readCache(imageUlr); bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); mCachePhoto.addBitmapToCache(imageUlr, bitmap); return bitmap; } return bitmap; } @Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); ImageView imageView; if (mGridView != null) { imageView = mGridView.findViewWithTag(mTag); } else if (mListView != null) { imageView = mListView.findViewWithTag(mTag); } else { imageView = this.mImageView; } if (imageView != null && bitmap!= null) { imageView.setImageBitmap(bitmap); } } private Bitmap downLoadBitmap(String imageUlr) { //如果没有缓存 就进行请求,然后进行缓存 Bitmap bitmap = null; byte[] request = new NetRequest().request(imageUlr); if (request != null){ bitmap = BitmapFactory.decodeByteArray(request, 0, request.length); } if (bitmap != null) { //将图片 缓存到内存 mCachePhoto.addBitmapToCache(imageUlr, bitmap); //将图片 缓存到磁盘 boolean b = mDataCache.writeData(imageUlr, bitmap); if (!b) { Log.e("PhotoCache", "磁盘缓存图片失败"); } return bitmap; } else { return null; } } }加了一个 LruCache 和 一个异步任务,具体的注释都在上面了。使用如下在ListView 中使用public class MainActivity extends PermissionCheck { private DiskBitmapCache mDisk; private LruCachePhoto mCache; @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mCache = new LruCachePhoto(); mDisk = new DiskBitmapCache(MainActivity.this, "456"); final ListView listView = findViewById(R.id.listview); listView.setAdapter(new BaseAdapter() { @Override public int getCount() { return Images.imageThumbUrls.length; } @Override public Object getItem(int position) { return null; } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { View view; if (convertView == null) { view = LayoutInflater.from(MainActivity.this).inflate(R.layout.photo_layout, null); } else { view = convertView; } String url = Images.imageThumbUrls[position]; ImageView imageView = view.findViewById(R.id.photo); imageView.setImageResource(R.drawable.log); imageView.setTag(url); BitmapWorkerTask task = new BitmapWorkerTask(mCache, mDisk, listView, url); task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url); return view; } }); } } 在上面给 缓存的文件夹名字起名为 456,然后就是一个ListView 了,注意,在getView 里面给 ImageView 设置了一个Tag ,这个非常重要,如果没有这个 Tag,我们将无法找到显示 ImageView ,还有 DiskBitmapCache 和 LruCachePhoto必须是全局的。效果如下在GridView 中使用public class GridViewActvity extends AppCompatActivity { private DiskBitmapCache mDisk; private LruCachePhoto mCache; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_grid_view); mCache = new LruCachePhoto(); mDisk = new DiskBitmapCache(this, "000"); final GridView gridView= findViewById(R.id.gridView); gridView.setAdapter(new BaseAdapter() { @Override public int getCount() { return Images.imageThumbUrls.length; } @Override public Object getItem(int position) { return null; } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { View view; if (convertView == null) { view = LayoutInflater.from(GridViewActvity.this).inflate(R.layout.photo_layout, null); } else { view = convertView; } String url = Images.imageThumbUrls[position]; ImageView imageView = view.findViewById(R.id.photo); imageView.setImageResource(R.drawable.log); imageView.setTag(url); BitmapWorkerTask task = new BitmapWorkerTask(mCache, mDisk, gridView, url); task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url); return view; } }); } }使用方式一样,注意别忘了 Tag 和 DiskBitmapCache 和 LruCachePhoto必须是全局的。效果如下下面看一下缓存目录
文章
缓存
2023-01-18
高性能图片优化方案
目录介绍01.图片基础概念介绍1.1 图片占用内存介绍1.2 加载网络图片流程1.3 三方库加载图片逻辑1.4 从网络直接拉取图片1.5 加载图片的流程1.6 Bitmap能直接存储吗1.7 Bitmap创建流程1.8 图片框架如何设计02.图片内存计算方式2.1 如何计算占用内存2.2 上面计算内存对吗2.3 一个像素占用内存2.4 使用API获取内存2.5 影响Bitmap内存因素2.6 加载xhdpi和xxhdpi图片2.7 图片一些注意事项03.大图的内存优化3.1 常见图片压缩3.2 图片尺寸压缩3.3 图片质量压缩3.4 双线性采样压缩3.5 高清图分片加载3.6 图片综合压缩04.色彩格式及内存优化4.1 RGB颜色种类4.2 ARGB色彩模式4.3 改变色彩格式优化05.缓存的使用实践优化5.1 Lru内存缓存5.2 Lru内存注意事项5.3 使用Lru磁盘缓存06.不同版本对Bitmap管理6.1 演变进程6.2 管理Bitmap内存6.3 提高Bitmap复用07.图片其他方面优化7.1 减少PNG图片的使用7.2 控件切割圆角优化7.3 如何给图片置灰色7.4 如何处理图片旋转呢7.5 保存图片且刷相册7.6 统一图片域名优化7.7 优化H5图片加载7.8 优化图片阴影效果7.9 图片资源的压缩01.图片基础概念介绍1.1 图片占用内存介绍移动设备的系统资源有限,所以应用应该尽可能的降低内存的使用。在应用运行过程中,Bitmap (图片)往往是内存占用最大的一个部分,Bitmap 图片的加载和处理,通常会占用大量的内存空间,所以在操作 Bitmap 时,应该尽可能的小心。Bitmap 会消耗很多的内存,特别是诸如照片等内容丰富的大图。例如,一个手机拍摄的 2700 1900 像素的照片,需要 5.1M 的存储空间,但是在图像解码配置 ARGB_8888 时,它加载到内存需要 19.6M 内存空间(2592 1936 * 4 bytes),从而迅速消耗掉该应用的剩余内存空间。OOM 的问题也是我们常见的严重问题,OOM 的产生的一个主要场景就是在大图片分配内存的时候产生的,如果 APP 可用内存紧张,这时加载了一张大图,内存空间不足以分配该图片所需要的内存,就会产生 OOM,所以控制图片的高效使用是必备技能。1.2 加载网络图片流程这一部分压缩和缓存图片,在glide源码分析的文章里已经做出了比较详细的说明。在这里简单说一下图片请求加载过程……在使用App的时候,会经常需要加载一些网络图片,一般的操作步骤大概是这样的:第一步从网络加载图片:一般都是通过网络拉取的方式去服务器端获取到图片的文件流后,再通过BitmapFactory.decodeStream(InputStream)来加载图片Bitmap。第二步这种压缩图片:网络加载图片方式加载一两张图片倒不会出现问题,但是如果短时间内加载十几张或者几十张图片的时候,就很有可能会造成OOM(内存溢出),因为现在的图片资源大小都是非常大的,所以我们在加载图片之前还需要进行相应的图片压缩处理。第三步变换图片:比如需要裁剪,切割圆角,旋转,添加高斯模糊等属性。第四步缓存图片:但又有个问题来了,在使用移动数据的情况下,如果用户每次进入App的时候都会去进行网络拉取图片,这样就会非常的浪费数据流量,这时又需要对图片资源进行一些相应的内存缓存以及磁盘缓存处理,这样不仅节省用户的数据流量,还能加快图片的加载速度。第五步异步加载:虽然利用缓存的方式可以加快图片的加载速度,但当需要加载很多张图片的时候(例如图片墙瀑布流效果),就还需用到多线程来加载图片,使用多线程就会涉及到线程同步加载与异步加载问题。1.3 三方库加载图片逻辑先说出结论,目前市面较为常用的大概是Glide,Picasso,Fresco等。大概的处理图片涉及主要逻辑有:从网络或者本地等路径拉取图片;然后解码图片;然后进行压缩;接着会有图片常用圆角,模糊或者裁剪等处理;然后三级缓存加载的图片;当然加载图片过程涉及同步加载和异步加载;最后设置到具体view控件上。1.4 从网络直接拉取图片直接通过网络请求将网络图片转化成bitmap在这将采用最原生的网络请求方式HttpURLConnection方式进行图片获取。经过测试,请求8张图片,耗时毫秒值174。一般是通过get请求拉取图片的。这种方法应该是最基础的网络请求,大家也可以回顾一下,一般开发中很少用这种方式加载图片。具体可以看:ImageToolLib如何加载一个图片呢?可以看看BitmapFactory类为我们提供了四类方法来加载Bitmap:decodeFile、decodeResource、decodeStream、decodeByteArray;也就是说Bitmap,Drawable,InputStream,Byte[] 之间是可以进行转换。1.5 加载图片的流程搞清楚一个图片概念在电脑上看到的 png 格式或者 jpg 格式的图片,png(jpg) 只是这张图片的容器。是经过相对应的压缩算法将原图每个像素点信息转换用另一种数据格式表示。加载图片显示到手机通过代码,将这张图片加载进内存时,会先解析(也就是解码操作)图片文件本身的数据格式,然后还原为位图,也就是 Bitmap 对象。图片大小vs图片内存大小一张 png 或者 jpg 格式的图片大小,跟这张图片加载进内存所占用的大小完全是两回事。1.6 Bitmap能直接存储吗Bitmap基础概念Bitmap对象本质是一张图片的内容在手机内存中的表达形式。它将图片的内容看做是由存储数据的有限个像素点组成;每个像素点存储该像素点位置的ARGB值。每个像素点的ARGB值确定下来,这张图片的内容就相应地确定下来了。Bitmap本质上不能直接存储为什么?bitmap是一个对象,如果要存储成本地可以查看的图片文件,则必须对bitmap进行编码,然后通过io流写到本地file文件上。1.7 Bitmap创建流程对于图片OOM,可以发现一个现象。heapsize(虚拟机的内存配置)越大越不容易 OOM,Android8.0 及之后的版本更不容易 OOM,这个该如何理解呢?Bitmap对象内存的变化:在 Android 8.0 之前,Bitmap 像素占用的内存是在 Java heap 中分配的;8.0 及之后,Bitmap 像素占用的内存分配到了 Native Heap。由于 Native heap 的内存分配上限很大,32 位应用的可用内存在 3~4G,64 位上更大,虚拟内存几乎很难耗尽,所以推测 OOM 时 Java heap 中占用内存较多的对象是 Bitmap” 成立的情况下,应用更不容易 OOM。搞清楚Bitmap对象内存分配Bitmap 的构造方法是不公开的,在使用 Bitmap 的时候,一般都是通过 Bitmap、BitmapFactory 提供的静态方法来创建 Bitmap 实例。以 Bitmap.createBitmap 说明了 Bitmap 对象的主要创建过程分析,可以看到 Java Bitmap 对象是在 Native 层通过 NewObject 创建的。allocateJavaPixelRef,是 8.0 之前版本为 Bitmap 像素从 Java heap 申请内存。其核心原理是Bitmap 的像素是保存在 Java 堆上。allocateHeapBitmap,是 8.0 版本为 Bitmap 像素从 Native heap 申请内存。其核心原理主要是通过 calloc 为 Bitmap 的像素分配内存,这个分配就在 Native 堆上。更多详细内容可以看:Bitmap对象内存分配1.8 图片框架如何设计大多数图片框架加载流程概括来说,图片加载包含封装,解析,下载,解码,变换,缓存,显示等操作。图片框架是如何设计的封装参数:从指定来源,到输出结果,中间可能经历很多流程,所以第一件事就是封装参数,这些参数会贯穿整个过程;解析路径:图片的来源有多种,格式也不尽相同,需要规范化;比如glide可以加载file,io,id,网络等各种图片资源读取缓存:为了减少计算,通常都会做缓存;同样的请求,从缓存中取图片(Bitmap)即可;查找文件/下载文件:如果是本地的文件,直接解码即可;如果是网络图片,需要先下载;比如glide这块是发起一个请求解码:这一步是整个过程中最复杂的步骤之一,有不少细节;比如glide中解析图片数据源,旋转方向,图片头等信息变换和压缩:解码出Bitmap之后,可能还需要做一些变换处理(圆角,滤镜等),还要做图片压缩;缓存:得到最终bitmap之后,可以缓存起来,以便下次请求时直接取结果;比如glide用到三级缓存显示:显示结果,可能需要做些动画(淡入动画,crossFade等);比如glide设置显示的时候可以添加动画效果02.图片内存计算方式2.1 如何计算占用内存如果图片要显示下Android设备上,ImageView最终是要加载Bitmap对象的,就要考虑单个Bitmap对象的内存占用了,如何计算一张图片的加载到内存的占用呢?其实就是所有像素的内存占用总和:bitmap内存大小 = 图片长度 x 图片宽度 x 单位像素占用的字节数起决定因素就是最后那个参数了,Bitmap常见有2种编码方式:ARGB_8888和RGB_565,ARGB_8888每个像素点4个byte,RGB_565是2个byte,一般都采用ARGB_8888这种。那么常见的1080*1920的图片内存占用就是:1920 x 1080 x 4 = 7.9M2.2 上面计算内存对吗我看到好多博客都是这样计算的,但是这样算对吗?有没有哥们试验过这种方法正确性?我觉得看博客要对博主表示怀疑,论证别人写的是否正确。说出我的结论:上面2.1这种说法也对,但是不全对,没有说明场景,同时也忽略了一个影响项:Density。接下来看看源代码。inDensity默认为图片所在文件夹对应的密度;inTargetDensity为当前系统密度。加载一张本地资源图片,那么它占用的内存 = width height nTargetDensity/inDensity 一个像素所占的内存。@Nullable public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value, @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) { validate(opts); if (opts == null) { opts = new Options(); } if (opts.inDensity == 0 && value != null) { final int density = value.density; if (density == TypedValue.DENSITY_DEFAULT) { opts.inDensity = DisplayMetrics.DENSITY_DEFAULT; } else if (density != TypedValue.DENSITY_NONE) { opts.inDensity = density; } } if (opts.inTargetDensity == 0 && res != null) { opts.inTargetDensity = res.getDisplayMetrics().densityDpi; } return decodeStream(is, pad, opts); }正确说法,这个注意呢?计算公式如下所示对资源文件:width height nTargetDensity/inDensity nTargetDensity/inDensity 一个像素所占的内存;别的:width height 一个像素所占的内存;2.3 一个像素占用内存一个像素占用多大内存?Bitmap.Config用来描述图片的像素是怎么被存储的?ARGB_8888: 每个像素4字节. 共32位,默认设置。Alpha_8: 只保存透明度,共8位,1字节。ARGB_4444: 共16位,2字节。RGB_565:共16位,2字节,只存储RGB值。2.4 使用API获取内存Bitmap使用API获取内存getByteCount()getByteCount()方法是在API12加入的,代表存储Bitmap的色素需要的最少内存。API19开始getAllocationByteCount()方法代替了getByteCount()。getAllocationByteCount()API19之后,Bitmap加了一个Api:getAllocationByteCount();代表在内存中为Bitmap分配的内存大小。思考: getByteCount()与getAllocationByteCount()的区别?一般情况下两者是相等的;通过复用Bitmap来解码图片,如果被复用的Bitmap的内存比待分配内存的Bitmap大,那么getByteCount()表示新解码图片占用内存的大小(并非实际内存大小,实际大小是复用的那个Bitmap的大小),getAllocationByteCount()表示被复用Bitmap真实占用的内存大小(即mBuffer的长度)。在复用Bitmap的情况下,getAllocationByteCount()可能会比getByteCount()大。2.5 影响Bitmap内存因素影响Bitmap占用内存的因素:图片最终加载的分辨率;图片的格式(PNG/JPEG/BMP/WebP);图片所存放的drawable目录;图片属性设置的色彩模式;设备的屏幕密度;2.6 加载xhdpi和xxhdpi图片提个问题,加载xhdpi和xxhdpi中相同的图片,显示在控件上会一样吗?内存大小一样吗?为什么?肯定是不一样的。xhdpi:240dpi--320dpi,xxhdpi:320dpi--480dpi,app中设置的图片是如何从hdpi中查找的?首先计算dpi,比如手机分辨率是1920x1080,5.1寸的手机。那么得到的dpi公式是(√ ̄1920² + 1080²)/5.1 =2202/5.1= 431dpi。这样优先查找xxhdpi如果xxhdpi里没有查找图片,如果没有会往上找,遵循“先高再低”原则。如果xhdpi里有这个图片会使用xhdpi里的图片,这时候发现会比在xhdpi里的图片放大了。为何要引入不同hdpi的文件管理?比如:xxhdpi放94x94,xhdpi放74x74,hdpi放45x45,这样不管是什么样的手机图片都能在指定的比例显示。引入多种hdpi是为了让这个图片在任何手机上都是手机的这个比例。2.7 图片一些注意事项同样图片显示在大小不相同的ImageView上,内存是一样吗?图片占据内存空间大小与图片在界面上显示的大小没有关系。图片放在res不同目录,加载的内存是一样的吗?最终图片加载进内存所占据的大小会不一样,因为系统在加载 res 目录下的资源图片时,会根据图片存放的不同目录做一次分辨率的转换,而转换的规则是:新图的高度 = 原图高度 * (设备的 dpi / 目录对应的 dpi )03.大图的内存优化3.1 常见图片压缩常见压缩方法ApiBitmap.compress(),质量压缩,不会对内存产生影响;BitmapFactory.Options.inSampleSize,内存压缩;Bitmap.compress()质量压缩质量压缩,不会对内存产生影响。它是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,不会减少图片的像素。进过它压缩的图片文件大小会变小,但是解码成bitmap后占得内存是不变的。BitmapFactory.Options.inSampleSize内存压缩解码图片时,设置BitmapFactory.Options类的inJustDecodeBounds属性为true,可以在Bitmap不被加载到内存的前提下,获取Bitmap的原始宽高。而设置BitmapFactory.Options的inSampleSize属性可以真实的压缩Bitmap占用的内存,加载更小内存的Bitmap。设置inSampleSize之后,Bitmap的宽、高都会缩小inSampleSize倍。例如:一张宽高为2048x1536的图片,设置inSampleSize为4之后,实际加载到内存中的图片宽高是512x384。占有的内存就是0.75M而不是12M,足足节省了15倍。备注:inSampleSize值的大小不是随便设、或者越大越好,需要根据实际情况来设置。inSampleSize比1小的话会被当做1,任何inSampleSize的值会被取接近2的幂值。3.2 图片尺寸压缩3.2.1 如何理解尺寸压缩通常在大多数情况下,图片的实际大小都比需要呈现的尺寸大很多。例如,我们的原图是一张 2700 1900 像素的照片,加载到内存就需要 19.6M 内存空间,但是,我们需要把它展示在一个列表页中,组件可展示尺寸为 270 190,这时,我们实际上只需要一张原图的低分辨率的缩略图即可(与图片显示所对应的 UI 控件匹配),那么实际上 270 * 190 像素的图片,只需要 0.2M 的内存即可。可以看到,优化前后相差了 98 倍,原来显示 1 张,现在可以显示 98 张图片,效果非常显著。既然在对原图缩放可以显著减少内存大小,那么我们应该如何操作呢?先加载到内存,再进行操作吗,可以如果先加载到内存,好像也不太对,这样只接占用了 19.6M + 0.2M 2份内存了,而我们想要的是,在原图不加载到内存中,只接将缩放后的图片加载到内存中,可以实现吗?BitmapFactory 提供了从不同资源创建 Bitmap 的解码方法:decodeByteArray()、decodeFile()、decodeResource() 等。但是,这些方法在构造位图的时候会尝试分配内存,也就是它们会导致原图直接加载到内存了,不满足我们的需求。我们可以通过 BitmapFactory.Options 设置一些附加的标记,指定解码选项,以此来解决该问题。如何操作呢?答案来了:将 inJustDecodeBounds 属性设置为 true,可以在解码时避免内存的分配,它会返回一个 null 的 Bitmap ,但是可以获取 outWidth、outHeight 和 outMimeType 值。利用该属性,我们就可以在图片不占用内存的情况下,在图片压缩之前获取图片的尺寸。怎样才能对图片进行压缩呢?通过设置BitmapFactory.Options中inSampleSize的值就可以实现。其计算方式大概就是:计算出实际宽高和目标宽高的比率,然后选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高。3.2.2 设置BitmapFactory.Options属性大概步骤如下所示要将BitmapFactory.Options的inJustDecodeBounds属性设置为true,解析一次图片。注意这个地方是核心,这个解析图片并没有生成bitmap对象(也就是说没有为它分配内存控件),而仅仅是拿到它的宽高等属性。然后将BitmapFactory.Options连同期望的宽度和高度一起传递到到calculateInSampleSize方法中,就可以得到合适的inSampleSize值了。这一步会压缩图片。之后再解析一次图片,使用新获取到的inSampleSize值,并把inJustDecodeBounds设置为false,就可以得到压缩后的图片了。此时才正式创建了bitmap对象,由于前面已经对它压缩了,所以你会发现此时所占内存大小已经很少了。具体的实现代码public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小 final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); // 调用上面定义的方法计算inSampleSize值 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 使用获取到的inSampleSize值再次解析图片 options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); }思考:inJustDecodeBounds这个参数是干什么的?如果设置为true则表示decode函数不会生成bitmap对象,仅是将图像相关的参数填充到option对象里,这样我们就可以在不生成bitmap而获取到图像的相关参数了。为何设置两次inJustDecodeBounds属性?第一次:设置为true则表示decode函数不会生成bitmap对象,仅是将图像相关的参数填充到option对象里,这样我们就可以在不生成bitmap而获取到图像的相关参数。第二次:将inJustDecodeBounds设置为false再次调用decode函数时就能生成bitmap了。而此时的bitmap已经压缩减小很多了,所以加载到内存中并不会导致OOM。3.3 图片质量压缩在Android中,对图片进行质量压缩,通常我们的实现方式如下所示://quality 为0~100,0表示最小体积,100表示最高质量,对应体积也是最大 bitmap.compress(Bitmap.CompressFormat.JPEG, quality , outputStream);在上述代码中,我们选择的压缩格式是CompressFormat.JPEG,除此之外还有两个选择:其一,CompressFormat.PNG,PNG格式是无损的,它无法再进行质量压缩,quality这个参数就没有作用了,会被忽略,所以最后图片保存成的文件大小不会有变化;其二,CompressFormat.WEBP,这个格式是google推出的图片格式,它会比JPEG更加省空间,经过实测大概可以优化30%左右。Android质量压缩逻辑,函数compress经过一连串的java层调用之后,最后来到了一个native函数:具体看:Bitmap.cpp,最后调用了函数encoder->encodeStream(…)编码保存本地。该函数是调用skia引擎来对图片进行编码压缩。3.4 双线性采样压缩双线性采样(Bilinear Resampling)在 Android 中的使用方式一般有两种:bm = Bitmap.createScaledBitmap(bitmap, bitmap.getWidth()/2, bitmap.getHeight()/2, true); //或者直接使用 matrix 进行缩放 Matrix matrix = new Matrix(); matrix.setScale(0.5f, 0.5f); bm = Bitmap.createBitmap(bitmap, 0, 0, bit.getWidth(), bit.getHeight(), matrix, true);看源码可以知道createScaledBitmap函数最终也是使用第二种方式的matrix进行缩放双线性采样使用的是双线性內插值算法,这个算法不像邻近点插值算法一样,直接粗暴的选择一个像素,而是参考了源像素相应位置周围2x2个点的值,根据相对位置取对应的权重,经过计算之后得到目标图像。3.5 高清图分片加载适用场景 : 当一张图片非常大 , 在手机中只需要显示其中一部分内容 , BitmapRegionDecoder 非常有用 。主要作用 : BitmapRegionDecoder 可以从图像中 解码一个矩形区域 。相当于手在滑动的过程中,计算当前显示区域的图片绘制出来。基本使用流程 : 先创建,后解码 。调用 newInstance 方法 , 创建 BitmapRegionDecoder 对象 ;然后调用 decodeRegion 方法 , 获取指定 Rect 矩形区域的解码后的 Bitmap 对象3.6 图片综合压缩一般情况下图片综合压缩的整体思路如下:第一步进行采样率压缩;第二步进行宽高的等比例压缩(微信对原图和缩略图限制了最大长宽或者最小长宽);第三步就是对图片的质量进行压缩(一般75或者70);第四步就是采用webP的格式。关于图片压缩的综合案例如下具体可以参考:CompressServer04.色彩格式及内存优化4.1 RGB颜色种类RGB 色彩模式是工业界的一种颜色标准通过对红(R)、绿(G)、蓝(B)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色的,RGB即是代表红、绿、蓝三个通道的颜色,这个标准几乎包括了人类视力所能感知的所有颜色,是运用最广的颜色系统之一。Android 中,像素的存储方式使用的色彩模式正是 RGB 色彩模式。4.2 ARGB色彩模式在 Android 中,我们常见的一些颜色设置,都是 RGB 色彩模式来描述像素颜色的,并且他们都带有透明度通道,也就是所谓的 ARGB。例如,我们常见的颜色定义如下://在代码中定义颜色值:蓝色 public final int blue=0xff0000ff; //或者在xml中定义: <drawable name="blue">#ff0000ff</drawable> 以上设置中,颜色值都是使用 16 进制的数字来表示的。以上颜色值都是带有透明度(透明通道)的颜色值,格式是 AARRGGBB,透明度、红色、绿色、蓝色四个颜色通道,各占有 2 位,也就是一个颜色通道,使用了 1 个字节来存储。4.3 改变色彩格式优化Android 中有多种 RGB 模式,我们可以设置不同的格式,来控制图片像素颜色的显示质量和存储空间。Android.graphics.Bitmap 类里有一个内部类 Bitmap.Config 类,它定义了可以在 Android 中使用的几种色彩格式:public enum Config { ALPHA_8 (1), RGB_565 (3), @Deprecated ARGB_4444 (4), ARGB_8888 (5), RGBA_F16 (6), HARDWARE (7); }解释一下这几个值分别代表了什么含义?我们已经知道了:A 代表透明度、R 代表红色、G 代表绿色、B 代表蓝色。ALPHA_8:表示,只存在 Alpha 通道,没有存储色彩值,只含有透明度,每个像素占用 1 个字节的空间。RGB_565:表示,R 占用 5 位二进制的位置,G 占用了6位,B 占用了 5 位。每个像素占用 2 个字节空间,并且不包含透明度。ARGB_4444:表示,A(透明度)、R(红色)、G(绿色)、B(蓝色)4个通道各占用 4 个 bit 位。每个像素占用 2 个字节空间。ARGB_8888:表示,A(透明度)、R(红色)、G(绿色)、B(蓝色)4个通道各占用 8 个 bit 位。每个像素占用 4 个字节空间。RGBA_F16:表示,每个像素存储在8个字节上。此配置特别适合广色域和HDR内容。HARDWARE:特殊配置,当位图仅存储在图形内存中时。 此配置中的位图始终是不可变的。那么开发中一般选择哪一种比较合适呢Android 中的图片在加载时,默认的色彩格式是 ARGB_8888,也就是每个像素占用 4 个字节空间,一张 2700 1900 像素的照片,加载到内存就需要 19.6M 内存空间(2592 1936 * 4 bytes)。如果图片在 UI 组件中显示时,不需要太高的图片质量,例如显示一张缩略图(不透明图片)等场景,这时,我们就没必要使用 ARGB_8888 的色彩格式了,只需要使用 RGB_565 模式即可满足显示的需要。那么,我们的优化操作就可以是:将 2700 1900 像素的原图,压缩到原图的低分辨率的缩略图 270 190 像素的图片,这时需要 0.2M 的内存。也就是从 19.6M内存,压缩为 0.2 M内存。我们还可以进一步优化色彩格式,由 ARGB_8888 改为 RGB_565 模式,这时,目标图片需要的内存就变为 270 190 2 = 0.1M 了。图片内存空间又减小了一倍。05.缓存的使用实践优化5.1 Lru内存缓存LruCache 类特别适合用来缓存 Bitmap,它使用一个强引用的 LinkedHashMap 保存最近引用的对象,并且在缓存超出设定大小时,删除最近最少使用的对象。给 LruCache 确定一个合适的缓存大小非常重要,我们需要考虑几个因素:应用剩余多少可用内存?需要有多少张图片同时显示到屏幕上?有多少图片需要准备好以便马上显示到屏幕?设备的屏幕大小和密度是多少?高密度的设备需要更大的缓存空间来缓存同样数量的图片。Bitmap 的尺寸配置是多少,花费多少内存?图片被访问的频率如何?如果其中一些比另外一些访问更频繁,那么我们可能希望在内存中保存那些最常访问的图片,或者根据访问频率给 Bitmap 分组,为不同的 Bitmap 组设置多个 LruCache 对象。是否可以在缓存图片的质量和数量之间寻找平衡点?有时,保存大量低质量的 Bitmap 会非常有用,加载更高质量的图片的任务可以交给另外一个后台线程处理。缓存太小会导致额外的花销却没有明显的好处,缓存太大同样会导致 java.lang.OutOfMemory 的异常,并且使得你的程序只留下小部分的内存用来工作(缓存占用太多内存,导致其他操作会因为内存不够而抛出异常)。所以,我们需要分析实际情况之后,提出一个合适的解决方案。LruCache是Android提供的一个缓存类,通常运用于内存缓存LruCache是一个泛型类,它的底层是用一个LinkedHashMap以强引用的方式存储外界的缓存对象来实现的。为什么使用LinkedHashMap来作为LruCache的存储,是因为LinkedHashMap有两种排序方式,一种是插入排序方式,一种是访问排序方式,默认情况下是以访问方式来存储缓存对象的;LruCache提供了get和put方法来完成缓存的获取和添加,当缓存满时,会将最近最少使用的对象移除掉,然后再添加新的缓存对象。如下源码所示,底层是LinkedHashMap。public LruCache(int maxSize) { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } this.maxSize = maxSize; this.map = new LinkedHashMap<K, V>(0, 0.75f, true); }在使用LruCache的时候,首先需要获取当前设备的内存容量,通常情况下会将总容量的八分之一作为LruCache的容量,然后重写LruCache的sizeof方法,sizeof方法用于计算缓存对象的大小,单位需要与分配的容量的单位一致;// 获取系统最大缓存 int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // set LruCache size; // 使用最大可用内存值的1/8作为缓存的大小 int cacheSize = maxMemory / 8; LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(@NonNull String uri, @NonNull Bitmap bitmap) { // 重写此方法来衡量每张图片的大小,默认返回图片数量 return bitmap.getRowBytes() * bitmap.getHeight() / 1024; } }; //插入对象 memoryCache.put(key, bitmap); //取出对象 memoryCache.get(key);如何淘汰缓存这个就要看LinkedHashMap集合的特点呢!LinkedHashMap 构造函数的第三个参数:accessOrder,传入true时, 元素会按访问顺序排列,最后访问的在遍历器最后端。在进行淘汰时,移除遍历器前端的元素,直至缓存总大小降低到指定大小以下。5.2 Lru内存注意事项看一个真实的场景假设我们的LruCache可以缓存80张,每次刷新从网络获取20张图片且不重复,那么在刷新第五次的时候,根据LruCache缓存的规则,第一次刷新的20张图片就会从LruCache中移出,处于等待被系统GC的状态。如果我们继续刷新n次,等待被回收的张数就会累积到 20 * n 张。会出现什么问题会出现大量的Bitmap内存碎片,我们不知道系统什么时候会触发GC回收掉这些无用的Bitmap,对于内存是否会溢出,是否会频繁GC导致卡顿等未知问题。解决方案该怎么做?第一种:在3.0以后引入了 BitmapFactory.Options.inBitmap,如果设置此项,需要解码的图片就会尝试使用该Bitmap的内存,这样取消了内存的动态分配,提高了性能,节省了内存。第二种:把处于无用的状态的Bitmap放入SoftReference。SoftReference引用的对象会在内存溢出之前被回收。关于Lru缓存案例和代码可以参考:AppLruCache5.3 使用Lru磁盘缓存内存缓存能够提高访问最近用过的 Bitmap 的速度,但是我们无法保证最近访问过的 Bitmap 都能够保存在缓存中。像类似 GridView 等需要大量数据填充的控件很容易就会用尽整个内存缓存。另外,我们的应用可能会被类似打电话等行为而暂停并退到后台,因为后台应用可能会被杀死,那么内存缓存就会被销毁,里面的 Bitmap 也就不存在了。一旦用户恢复应用的状态,那么应用就需要重新处理那些图片。磁盘缓存可以用来保存那些已经处理过的 Bitmap,它还可以减少那些不再内存缓存中的 Bitmap 的加载次数。当然从磁盘读取图片会比从内存要慢,而且由于磁盘读取操作时间是不可预期的,读取操作需要在后台线程中处理。注意:如果图片会被更频繁的访问,使用 ContentProvider 或许会更加合适,比如在图库应用中。注意:因为初始化磁盘缓存涉及到 I/O 操作,所以它不应该在主线程中进行。但是这也意味着在初始化完成之前缓存可以被访问。为了解决这个问题,在上面的实现中,有一个锁对象(lock object)来确保在磁盘缓存完成初始化之前,应用无法对它进行读取。内存缓存的检查是可以在 UI 线程中进行的,磁盘缓存的检查需要在后台线程中处理。磁盘操作永远都不应该在 UI 线程中发生。当图片处理完成后,Bitmap 需要添加到内存缓存与磁盘缓存中,方便之后的使用。06.不同版本对Bitmap管理6.1 演变进程Android 2.3.3 (API level 10)以及之前,一个 Bitmap 的像素数据是存放在 Native 内存空间中的。这些数据与 Bitmap 对象本身是隔离的,Bitmap 本身被存放在 Dalvik 堆中。并且无法预测在 Native 内存中的像素级数据何时会被释放,这意味着程序容易超过它的内存限制并且崩溃。Android 3.0 (API Level 11)开始像素数据则是与 Bitmap 本身一起存放在 Dalvik 堆中。Android 8.0(Android O)及之后的版本中Bitmap 的像素数据的内存分配又回到了 Native 层,它是在 Native 堆空间进行分配的。6.2 管理Bitmap内存管理 Android 2.3.3 及以下版本的内存使用在 Android 2.3.3 (API level 10) 以及更低版本上,推荐使用 recycle() 方法。 如果在应用中显示了大量的 Bitmap 数据,我们很可能会遇到 OutOfMemoryError 的错误。 recycle() 方法可以使得程序更快的释放内存。管理 Android 3.0 及其以上版本的内存从 Android 3.0 (API Level 11)开始,引进了 BitmapFactory.Options.inBitmap 字段。 如果使用了这个设置字段,decode 方法会在加载 Bitmap 数据的时候去重用已经存在的 Bitmap。这意味着 Bitmap 的内存是被重新利用的,这样可以提升性能,并且减少了内存的分配与回收。然而,使用 inBitmap 有一些限制,特别是在Android 4.4 (API level 19)之前,只有同等大小的位图才可以被重用。管理 Android 8.0 及其以上版本的内存在 Android 8.0 及其以上版本,处理内存,也遵循 Android 3.0 以上版本同样的方式。同时,图片像素数据存储在 native 层,并且不占用 Java 堆的空间,这也代表着我们拥有更大的图片存储空间,可以加载质量更高、数据更多的图片到内存中。但是,内存依然不是无限的,应用还是要受到手机内存的限制,所以一定要注意这一点。6.3 提高Bitmap复用Android3.0之后,并没有强调Bitmap.recycle();而是强调Bitmap的复用。使用LruCache对Bitmap进行缓存,当再次使用到这个Bitmap的时候直接获取,而不用重走编码流程。Android3.0(API 11之后)引入了BitmapFactory.Options.inBitmap字段,设置此字段之后解码方法会尝试复用一张存在的Bitmap。这意味着Bitmap的内存被复用,避免了内存的回收及申请过程,显然性能表现更佳。使用这个字段有几点限制:声明可被复用的Bitmap必须设置inMutable为true;Android4.4(API 19)之前只有格式为jpg、png,同等宽高(要求苛刻),inSampleSize为1的Bitmap才可以复用;Android4.4(API 19)之前被复用的Bitmap的inPreferredConfig会覆盖待分配内存的Bitmap设置的inPreferredConfig;Android4.4(API 19)之后被复用的Bitmap的内存必须大于需要申请内存的Bitmap的内存;Android4.4(API 19)之前待加载Bitmap的Options.inSampleSize必须明确指定为1。Bitmap复用的实验,代码如下所示,然后看打印的日志信息从内存地址的打印可以看出,两个对象其实是一个对象,Bitmap复用成功;bitmapReuse占用的内存(4346880)正好是bitmap占用内存(1228800)的四分之一;getByteCount()获取到的是当前图片应当所占内存大小,getAllocationByteCount()获取到的是被复用Bitmap真实占用内存大小。虽然bitmapReuse的内存只有4346880,但是因为是复用的bitmap的内存,因而其真实占用的内存大小是被复用的bitmap的内存大小(1228800)。这也是getAllocationByteCount()可能比getByteCount()大的原因。@RequiresApi(api = Build.VERSION_CODES.KITKAT) private void initBitmap() { BitmapFactory.Options options = new BitmapFactory.Options(); // 图片复用,这个属性必须设置; options.inMutable = true; // 手动设置缩放比例,使其取整数,方便计算、观察数据; options.inDensity = 320; options.inTargetDensity = 320; Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bg_autumn_tree_min, options); // 对象内存地址; Log.i("ycBitmap", "bitmap = " + bitmap); Log.i("ycBitmap", "ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount()); // 使用inBitmap属性,这个属性必须设置; options.inBitmap = bitmap; options.inDensity = 320; // 设置缩放宽高为原始宽高一半; options.inTargetDensity = 160; options.inMutable = true; Bitmap bitmapReuse = BitmapFactory.decodeResource(getResources(), R.drawable.bg_kites_min, options); // 复用对象的内存地址; Log.i("ycBitmap", "bitmapReuse = " + bitmapReuse); Log.i("ycBitmap", "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount()); Log.i("ycBitmap", "bitmapReuse:ByteCount = " + bitmapReuse.getByteCount() + ":::bitmapReuse:AllocationByteCount = " + bitmapReuse.getAllocationByteCount()); //11-26 18:24:07.971 15470-15470/com.yc.ycbanner I/ycBitmap: bitmap = android.graphics.Bitmap@9739bff //11-26 18:24:07.972 15470-15470/com.yc.ycbanner I/ycBitmap: bitmap:ByteCount = 4346880:::bitmap:AllocationByteCount = 4346880 //11-26 18:24:07.994 15470-15470/com.yc.ycbanner I/ycBitmap: bitmapReuse = android.graphics.Bitmap@9739bff //11-26 18:24:07.994 15470-15470/com.yc.ycbanner I/ycBitmap: bitmap:ByteCount = 1228800:::bitmap:AllocationByteCount = 4346880 //11-26 18:24:07.994 15470-15470/com.yc.ycbanner I/ycBitmap: bitmapReuse:ByteCount = 1228800:::bitmapReuse:AllocationByteCount = 4346880 }07.图片其他方面优化7.1 减少PNG图片的使用这里要介绍一种新的图片格式:Webp,它是由 Google 推出的一种既保留 png 格式的优点,又能够减少图片大小的一种新型图片格式。在 Android 4.0(API level 14) 中支持有损的 WebP 图像,在 Android 4.3(API level 18) 和更高版本中支持无损和透明的 WebP 图像。注意一下,Webp格式图片仅仅只是减少图片的质量大小,并不会减少加载图片后的内存占用。7.2 切割圆角优化方案1:直接采用Canvas.clipPath 相关api,裁剪出一个圆角区域。该方案简单暴力,通用性强。如果只是一个静态的单图视图,该方法问题不大,但如果是复杂页面,滚动的时候,测试就会跟你说,页面卡顿了,要优化。原因就是 Canvas.clip的相关api损耗相对较大。方案2:系统提供的CardView设置圆角把原来全工程各个视频控件和图片控件的外层,都加上一层CardView。改造成本大,布局层级更深一层,layout时间加长。方案3:使用setXfermode法此种方式就是再new一个相同尺寸的bitmap,然后使用paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));先画圆角矩形,再画原始bitmap,然后就得到了一个圆角的bitmap。早期用得较多,占用bitmap双倍内存。方案4:图片加载库比如Glide,Fresco等在底层,无非也是使用上面的这两种种方式。早期的使用setXfermode来实现,后来使用BitmapShader实现。使用简单,稳定。方案5:遮罩还是使用setXfermode,不过与方法一不同的是:不对图片作任何更改,只在圆角之外再画一层与背景颜色相同的四个角来遮挡,在视觉上造成圆角图片的效果。那个切割圆角该怎么优化呢?使用方案3,可以采用自定义view,支持LinearLayout、RelativeLayout、FrameLayout、ConstraintLayout、ImageView、TextView、View、Button等设置圆角。具体案例可见:RoundCorners7.3 如何给图片置灰色大概的操作步骤。具体可以参考:PicCalculateUtils第一步:获取原始图片的宽高,然后创建一个bitmap可变位图对象。第二步:创建画板canvas对象,然后创建画笔paint。然后调用canvas.drawBitmap方法绘制图片第三步:对画笔进行修饰,设置画笔颜色属性,这里使用到了ColorMatrix,核心就是设置饱和度为0,即可绘制灰色内容7.4 如何处理图片旋转呢在Android中使用ImageView显示图片的时候发现图片显示不正,方向偏了或者倒过来了。解决这个问题很自然想到的两步走,首先是要自动识别图像方向,计算旋转角度,然后对图像进行旋转并显示。识别图像方向首先在这里提一个概念EXIF(Exchangeable Image File Format,可交换图像文件)。简而言之,Exif是一个标准,用于电子照相机(也包括手机、扫描器等)上,用来规范图片、声音、视屏以及它们的一些辅助标记格式。Exif支持的格式如下:图像;压缩图像文件:JPEG、DCT;非压缩图像文件:TIFF;音频;RIFF、WAVAndroid提供了对JPEG格式图像Exif接口支持,可以读取JPEG文件metadata信息,参见ExifInterface。这些Metadata信息总的来说大致分为三类:日期时间、空间信息(经纬度、高度)、Camera信息(孔径、焦距、旋转角、曝光量等等)。关于图像旋转获取了图片的旋转方向后,然后再设置图像旋转。最后Bitmap提供的静态createBitmap方法,可以对图片设置旋转角度。具体看:PicCalculateUtils7.5 保存图片且刷相册大概的操作步骤如下所示。具体可看:ImageSaveUtils第一步:创建图片文件,然后将bitmap对象写到图片文件中第二步:通过MediaStore将图片插入到共享目录相册中第三步:发送通知,通知相册中刷新插入图片的数据。注意,获取图片资源uri刷新即可,避免刷新所有数据造成等待时间过长。7.6 统一图片域名优化域名统一减少了10%+的重复图片下载和内存消耗。同时减少之前多域名图片加载时重复创建HTTPS请求的过程,减少图片加载时间。7.7 优化H5图片加载通过拦截WebView图片加载的方式,让原生图片库来下载图片之后传递图片二进制数据给WebView显示。采用OkHttp拦截资源缓存,下面是大概的思路。缓存的入口从shouldInterceptRequest出发第一步,拿到WebResourceRequest对象中请求资源的url还有header,如果开发者设置不缓存则返回null第二步,如果缓存,通过url判断拦截资源的条件,过滤非http,音视频等资源,这个是可自由配置缓存内容比如css,png,jpg,xml,txt等第三步,判断本地是否有OkHttp缓存数据,如果有则直接读取本地资源,通过url找到对应的path路径,然后读取文件流,组装数据返回。第四步,如果没有缓存数据,创建OkHttp的Request请求,将资源网络请求交给okHttp来处理,并且用它自带的缓存功能,当然如果是请求失败或者异常则返回null,否则返回正常数据关于webView图片缓存的方案,可以直接参考:YCWebView7.8 优化图片阴影效果阴影效果有哪些实现方式第一种:使用CardView,但是不能设置阴影颜色第二种:采用shape叠加,存在后期UI效果不便优化第三种:UI切图第四种:自定义View第五种:自定义Drawable否定上面前两种方案原因分析?第一个方案的CardView渐变色和阴影效果很难控制,只能支持线性或者环装形式渐变,这种不满足需要,因为阴影本身是一个四周一层很淡的颜色包围,在一个矩形框的层面上颜色大概一致,而且这个CardView有很多局限性,比如不能修改阴影的颜色,不能修改阴影的深浅。所以这个思路无法实现这个需求。第二个采用shape叠加,可以实现阴影效果,但是影响UI,且阴影部分是占像素的,而且不灵活。第三个方案询问了一下ui。他们给出的结果是如果使用切图的话那标注的话很难标,身为一个优秀的设计师大多对像素点都和敏感,界面上的像素点有一点不协调那都是无法容忍的。网上一些介绍阴影效果方案所有在深奥的技术,也都是为需求做准备的。也就是需要实践并且可以用到实际开发中,这篇文章不再抽象介绍阴影效果原理,理解三维空间中如何处理偏移光线达到阴影视差等,网上看了一些文章也没看明白或者理解。这篇博客直接通过调用api实现预期的效果。多个drawable叠加,使用layer-list可以将多个drawable按照顺序层叠在一起显示,默认情况下,所有的item中的drawable都会自动根据它附上view的大小而进行缩放,layer-list中的item是按照顺序从下往上叠加的,即先定义的item在下面,后面的依次往上面叠放阴影是否占位使用CardView阴影不占位,不能设置阴影颜色和效果使用shape阴影是可以设置阴影颜色,但是是占位的几种方案优缺点对比分析CardView 优点:自带功能实现简单 缺点:自带圆角不一定可适配所有需求layer(shape叠加) 优点:实现形式简单 缺点:效果一般自定义实现 优点:实现效果好可配置能力高 缺点:需要开发者自行开发关于解决阴影效果具体各种方案的对比可以参考这个demo:AppShadowLib7.9 图片资源的压缩我们应用中使用的图片,设计师出的原图通常都非常大,他们通常会使用工具,经过一定的压缩,缩减到比较小一些的大小。但是,这些图片通常都有一定的可压缩空间,我在之前的项目中,对图片进行了二次压缩,整体压缩率达到了 40%~50% ,效果还是非常不错的。这里介绍下常用的,图片压缩的方法:使用压缩工具对图片进行二次压缩。根据最终图片是否需要透明度展示,优先选择不透明的图片格式,例如,我们应该避免使用 png 格式的图片。对于色彩简单,例如,一些背景之类的图片,可以选择使用布局文件来定义(矢量图),这样就会非常节省内存了。如果包含透明度,优先使用 WebP 等格式图像。图片在上线前进行压缩处理,不但可以减少内存的使用,如果图片是网络获取的,也可以减少网络加载的流量和时间。推荐一个图片压缩网站:tinypng网站
文章
存储  ·  缓存  ·  编解码  ·  移动开发  ·  算法  ·  前端开发  ·  Java  ·  API  ·  Android开发  ·  数据格式
2022-10-16
高性能图片优化方案
目录介绍01.图片基础概念介绍1.1 图片占用内存介绍1.2 加载网络图片流程1.3 三方库加载图片逻辑1.4 从网络直接拉取图片1.5 加载图片的流程1.6 Bitmap能直接存储吗1.7 Bitmap创建流程1.8 图片框架如何设计02.图片内存计算方式2.1 如何计算占用内存2.2 上面计算内存对吗2.3 一个像素占用内存2.4 使用API获取内存2.5 影响Bitmap内存因素2.6 加载xhdpi和xxhdpi图片2.7 图片一些注意事项03.大图的内存优化3.1 常见图片压缩3.2 图片尺寸压缩3.3 图片质量压缩3.4 双线性采样压缩3.5 高清图分片加载3.6 图片综合压缩04.色彩格式及内存优化4.1 RGB颜色种类4.2 ARGB色彩模式4.3 改变色彩格式优化05.缓存的使用实践优化5.1 Lru内存缓存5.2 Lru内存注意事项5.3 使用Lru磁盘缓存06.不同版本对Bitmap管理6.1 演变进程6.2 管理Bitmap内存6.3 提高Bitmap复用07.图片其他方面优化7.1 减少PNG图片的使用7.2 控件切割圆角优化7.3 如何给图片置灰色7.4 如何处理图片旋转呢7.5 保存图片且刷相册7.6 统一图片域名优化7.7 优化H5图片加载7.8 优化图片阴影效果7.9 图片资源的压缩01.图片基础概念介绍1.1 图片占用内存介绍移动设备的系统资源有限,所以应用应该尽可能的降低内存的使用。在应用运行过程中,Bitmap (图片)往往是内存占用最大的一个部分,Bitmap 图片的加载和处理,通常会占用大量的内存空间,所以在操作 Bitmap 时,应该尽可能的小心。Bitmap 会消耗很多的内存,特别是诸如照片等内容丰富的大图。例如,一个手机拍摄的 2700 1900 像素的照片,需要 5.1M 的存储空间,但是在图像解码配置 ARGB_8888 时,它加载到内存需要 19.6M 内存空间(2592 1936 * 4 bytes),从而迅速消耗掉该应用的剩余内存空间。OOM 的问题也是我们常见的严重问题,OOM 的产生的一个主要场景就是在大图片分配内存的时候产生的,如果 APP 可用内存紧张,这时加载了一张大图,内存空间不足以分配该图片所需要的内存,就会产生 OOM,所以控制图片的高效使用是必备技能。1.2 加载网络图片流程这一部分压缩和缓存图片,在glide源码分析的文章里已经做出了比较详细的说明。在这里简单说一下图片请求加载过程……在使用App的时候,会经常需要加载一些网络图片,一般的操作步骤大概是这样的:第一步从网络加载图片:一般都是通过网络拉取的方式去服务器端获取到图片的文件流后,再通过BitmapFactory.decodeStream(InputStream)来加载图片Bitmap。第二步这种压缩图片:网络加载图片方式加载一两张图片倒不会出现问题,但是如果短时间内加载十几张或者几十张图片的时候,就很有可能会造成OOM(内存溢出),因为现在的图片资源大小都是非常大的,所以我们在加载图片之前还需要进行相应的图片压缩处理。第三步变换图片:比如需要裁剪,切割圆角,旋转,添加高斯模糊等属性。第四步缓存图片:但又有个问题来了,在使用移动数据的情况下,如果用户每次进入App的时候都会去进行网络拉取图片,这样就会非常的浪费数据流量,这时又需要对图片资源进行一些相应的内存缓存以及磁盘缓存处理,这样不仅节省用户的数据流量,还能加快图片的加载速度。第五步异步加载:虽然利用缓存的方式可以加快图片的加载速度,但当需要加载很多张图片的时候(例如图片墙瀑布流效果),就还需用到多线程来加载图片,使用多线程就会涉及到线程同步加载与异步加载问题。1.3 三方库加载图片逻辑先说出结论,目前市面较为常用的大概是Glide,Picasso,Fresco等。大概的处理图片涉及主要逻辑有:从网络或者本地等路径拉取图片;然后解码图片;然后进行压缩;接着会有图片常用圆角,模糊或者裁剪等处理;然后三级缓存加载的图片;当然加载图片过程涉及同步加载和异步加载;最后设置到具体view控件上。1.4 从网络直接拉取图片直接通过网络请求将网络图片转化成bitmap在这将采用最原生的网络请求方式HttpURLConnection方式进行图片获取。经过测试,请求8张图片,耗时毫秒值174。一般是通过get请求拉取图片的。这种方法应该是最基础的网络请求,大家也可以回顾一下,一般开发中很少用这种方式加载图片。具体可以看:ImageToolLib如何加载一个图片呢?可以看看BitmapFactory类为我们提供了四类方法来加载Bitmap:decodeFile、decodeResource、decodeStream、decodeByteArray;也就是说Bitmap,Drawable,InputStream,Byte[] 之间是可以进行转换。1.5 加载图片的流程搞清楚一个图片概念在电脑上看到的 png 格式或者 jpg 格式的图片,png(jpg) 只是这张图片的容器。是经过相对应的压缩算法将原图每个像素点信息转换用另一种数据格式表示。加载图片显示到手机通过代码,将这张图片加载进内存时,会先解析(也就是解码操作)图片文件本身的数据格式,然后还原为位图,也就是 Bitmap 对象。图片大小vs图片内存大小一张 png 或者 jpg 格式的图片大小,跟这张图片加载进内存所占用的大小完全是两回事。1.6 Bitmap能直接存储吗Bitmap基础概念Bitmap对象本质是一张图片的内容在手机内存中的表达形式。它将图片的内容看做是由存储数据的有限个像素点组成;每个像素点存储该像素点位置的ARGB值。每个像素点的ARGB值确定下来,这张图片的内容就相应地确定下来了。Bitmap本质上不能直接存储为什么?bitmap是一个对象,如果要存储成本地可以查看的图片文件,则必须对bitmap进行编码,然后通过io流写到本地file文件上。1.7 Bitmap创建流程对于图片OOM,可以发现一个现象。heapsize(虚拟机的内存配置)越大越不容易 OOM,Android8.0 及之后的版本更不容易 OOM,这个该如何理解呢?Bitmap对象内存的变化:在 Android 8.0 之前,Bitmap 像素占用的内存是在 Java heap 中分配的;8.0 及之后,Bitmap 像素占用的内存分配到了 Native Heap。由于 Native heap 的内存分配上限很大,32 位应用的可用内存在 3~4G,64 位上更大,虚拟内存几乎很难耗尽,所以推测 OOM 时 Java heap 中占用内存较多的对象是 Bitmap” 成立的情况下,应用更不容易 OOM。搞清楚Bitmap对象内存分配Bitmap 的构造方法是不公开的,在使用 Bitmap 的时候,一般都是通过 Bitmap、BitmapFactory 提供的静态方法来创建 Bitmap 实例。以 Bitmap.createBitmap 说明了 Bitmap 对象的主要创建过程分析,可以看到 Java Bitmap 对象是在 Native 层通过 NewObject 创建的。allocateJavaPixelRef,是 8.0 之前版本为 Bitmap 像素从 Java heap 申请内存。其核心原理是Bitmap 的像素是保存在 Java 堆上。allocateHeapBitmap,是 8.0 版本为 Bitmap 像素从 Native heap 申请内存。其核心原理主要是通过 calloc 为 Bitmap 的像素分配内存,这个分配就在 Native 堆上。更多详细内容可以看:Bitmap对象内存分配1.8 图片框架如何设计大多数图片框架加载流程概括来说,图片加载包含封装,解析,下载,解码,变换,缓存,显示等操作。图片框架是如何设计的封装参数:从指定来源,到输出结果,中间可能经历很多流程,所以第一件事就是封装参数,这些参数会贯穿整个过程;解析路径:图片的来源有多种,格式也不尽相同,需要规范化;比如glide可以加载file,io,id,网络等各种图片资源读取缓存:为了减少计算,通常都会做缓存;同样的请求,从缓存中取图片(Bitmap)即可;查找文件/下载文件:如果是本地的文件,直接解码即可;如果是网络图片,需要先下载;比如glide这块是发起一个请求解码:这一步是整个过程中最复杂的步骤之一,有不少细节;比如glide中解析图片数据源,旋转方向,图片头等信息变换和压缩:解码出Bitmap之后,可能还需要做一些变换处理(圆角,滤镜等),还要做图片压缩;缓存:得到最终bitmap之后,可以缓存起来,以便下次请求时直接取结果;比如glide用到三级缓存显示:显示结果,可能需要做些动画(淡入动画,crossFade等);比如glide设置显示的时候可以添加动画效果02.图片内存计算方式2.1 如何计算占用内存如果图片要显示下Android设备上,ImageView最终是要加载Bitmap对象的,就要考虑单个Bitmap对象的内存占用了,如何计算一张图片的加载到内存的占用呢?其实就是所有像素的内存占用总和:bitmap内存大小 = 图片长度 x 图片宽度 x 单位像素占用的字节数起决定因素就是最后那个参数了,Bitmap常见有2种编码方式:ARGB_8888和RGB_565,ARGB_8888每个像素点4个byte,RGB_565是2个byte,一般都采用ARGB_8888这种。那么常见的1080*1920的图片内存占用就是:1920 x 1080 x 4 = 7.9M2.2 上面计算内存对吗我看到好多博客都是这样计算的,但是这样算对吗?有没有哥们试验过这种方法正确性?我觉得看博客要对博主表示怀疑,论证别人写的是否正确。说出我的结论:上面2.1这种说法也对,但是不全对,没有说明场景,同时也忽略了一个影响项:Density。接下来看看源代码。inDensity默认为图片所在文件夹对应的密度;inTargetDensity为当前系统密度。加载一张本地资源图片,那么它占用的内存 = width height nTargetDensity/inDensity 一个像素所占的内存。@Nullable public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value, @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) { validate(opts); if (opts == null) { opts = new Options(); } if (opts.inDensity == 0 && value != null) { final int density = value.density; if (density == TypedValue.DENSITY_DEFAULT) { opts.inDensity = DisplayMetrics.DENSITY_DEFAULT; } else if (density != TypedValue.DENSITY_NONE) { opts.inDensity = density; } } if (opts.inTargetDensity == 0 && res != null) { opts.inTargetDensity = res.getDisplayMetrics().densityDpi; } return decodeStream(is, pad, opts); }正确说法,这个注意呢?计算公式如下所示对资源文件:width height nTargetDensity/inDensity nTargetDensity/inDensity 一个像素所占的内存;别的:width height 一个像素所占的内存;2.3 一个像素占用内存一个像素占用多大内存?Bitmap.Config用来描述图片的像素是怎么被存储的?ARGB_8888: 每个像素4字节. 共32位,默认设置。Alpha_8: 只保存透明度,共8位,1字节。ARGB_4444: 共16位,2字节。RGB_565:共16位,2字节,只存储RGB值。2.4 使用API获取内存Bitmap使用API获取内存getByteCount()getByteCount()方法是在API12加入的,代表存储Bitmap的色素需要的最少内存。API19开始getAllocationByteCount()方法代替了getByteCount()。getAllocationByteCount()API19之后,Bitmap加了一个Api:getAllocationByteCount();代表在内存中为Bitmap分配的内存大小。思考: getByteCount()与getAllocationByteCount()的区别?一般情况下两者是相等的;通过复用Bitmap来解码图片,如果被复用的Bitmap的内存比待分配内存的Bitmap大,那么getByteCount()表示新解码图片占用内存的大小(并非实际内存大小,实际大小是复用的那个Bitmap的大小),getAllocationByteCount()表示被复用Bitmap真实占用的内存大小(即mBuffer的长度)。在复用Bitmap的情况下,getAllocationByteCount()可能会比getByteCount()大。2.5 影响Bitmap内存因素影响Bitmap占用内存的因素:图片最终加载的分辨率;图片的格式(PNG/JPEG/BMP/WebP);图片所存放的drawable目录;图片属性设置的色彩模式;设备的屏幕密度;2.6 加载xhdpi和xxhdpi图片提个问题,加载xhdpi和xxhdpi中相同的图片,显示在控件上会一样吗?内存大小一样吗?为什么?肯定是不一样的。xhdpi:240dpi--320dpi,xxhdpi:320dpi--480dpi,app中设置的图片是如何从hdpi中查找的?首先计算dpi,比如手机分辨率是1920x1080,5.1寸的手机。那么得到的dpi公式是(√ ̄1920² + 1080²)/5.1 =2202/5.1= 431dpi。这样优先查找xxhdpi如果xxhdpi里没有查找图片,如果没有会往上找,遵循“先高再低”原则。如果xhdpi里有这个图片会使用xhdpi里的图片,这时候发现会比在xhdpi里的图片放大了。为何要引入不同hdpi的文件管理?比如:xxhdpi放94x94,xhdpi放74x74,hdpi放45x45,这样不管是什么样的手机图片都能在指定的比例显示。引入多种hdpi是为了让这个图片在任何手机上都是手机的这个比例。2.7 图片一些注意事项同样图片显示在大小不相同的ImageView上,内存是一样吗?图片占据内存空间大小与图片在界面上显示的大小没有关系。图片放在res不同目录,加载的内存是一样的吗?最终图片加载进内存所占据的大小会不一样,因为系统在加载 res 目录下的资源图片时,会根据图片存放的不同目录做一次分辨率的转换,而转换的规则是:新图的高度 = 原图高度 * (设备的 dpi / 目录对应的 dpi )03.大图的内存优化3.1 常见图片压缩常见压缩方法ApiBitmap.compress(),质量压缩,不会对内存产生影响;BitmapFactory.Options.inSampleSize,内存压缩;Bitmap.compress()质量压缩质量压缩,不会对内存产生影响。它是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,不会减少图片的像素。进过它压缩的图片文件大小会变小,但是解码成bitmap后占得内存是不变的。BitmapFactory.Options.inSampleSize内存压缩解码图片时,设置BitmapFactory.Options类的inJustDecodeBounds属性为true,可以在Bitmap不被加载到内存的前提下,获取Bitmap的原始宽高。而设置BitmapFactory.Options的inSampleSize属性可以真实的压缩Bitmap占用的内存,加载更小内存的Bitmap。设置inSampleSize之后,Bitmap的宽、高都会缩小inSampleSize倍。例如:一张宽高为2048x1536的图片,设置inSampleSize为4之后,实际加载到内存中的图片宽高是512x384。占有的内存就是0.75M而不是12M,足足节省了15倍。备注:inSampleSize值的大小不是随便设、或者越大越好,需要根据实际情况来设置。inSampleSize比1小的话会被当做1,任何inSampleSize的值会被取接近2的幂值。3.2 图片尺寸压缩3.2.1 如何理解尺寸压缩通常在大多数情况下,图片的实际大小都比需要呈现的尺寸大很多。例如,我们的原图是一张 2700 1900 像素的照片,加载到内存就需要 19.6M 内存空间,但是,我们需要把它展示在一个列表页中,组件可展示尺寸为 270 190,这时,我们实际上只需要一张原图的低分辨率的缩略图即可(与图片显示所对应的 UI 控件匹配),那么实际上 270 * 190 像素的图片,只需要 0.2M 的内存即可。可以看到,优化前后相差了 98 倍,原来显示 1 张,现在可以显示 98 张图片,效果非常显著。既然在对原图缩放可以显著减少内存大小,那么我们应该如何操作呢?先加载到内存,再进行操作吗,可以如果先加载到内存,好像也不太对,这样只接占用了 19.6M + 0.2M 2份内存了,而我们想要的是,在原图不加载到内存中,只接将缩放后的图片加载到内存中,可以实现吗?BitmapFactory 提供了从不同资源创建 Bitmap 的解码方法:decodeByteArray()、decodeFile()、decodeResource() 等。但是,这些方法在构造位图的时候会尝试分配内存,也就是它们会导致原图直接加载到内存了,不满足我们的需求。我们可以通过 BitmapFactory.Options 设置一些附加的标记,指定解码选项,以此来解决该问题。如何操作呢?答案来了:将 inJustDecodeBounds 属性设置为 true,可以在解码时避免内存的分配,它会返回一个 null 的 Bitmap ,但是可以获取 outWidth、outHeight 和 outMimeType 值。利用该属性,我们就可以在图片不占用内存的情况下,在图片压缩之前获取图片的尺寸。怎样才能对图片进行压缩呢?通过设置BitmapFactory.Options中inSampleSize的值就可以实现。其计算方式大概就是:计算出实际宽高和目标宽高的比率,然后选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高。3.2.2 设置BitmapFactory.Options属性大概步骤如下所示要将BitmapFactory.Options的inJustDecodeBounds属性设置为true,解析一次图片。注意这个地方是核心,这个解析图片并没有生成bitmap对象(也就是说没有为它分配内存控件),而仅仅是拿到它的宽高等属性。然后将BitmapFactory.Options连同期望的宽度和高度一起传递到到calculateInSampleSize方法中,就可以得到合适的inSampleSize值了。这一步会压缩图片。之后再解析一次图片,使用新获取到的inSampleSize值,并把inJustDecodeBounds设置为false,就可以得到压缩后的图片了。此时才正式创建了bitmap对象,由于前面已经对它压缩了,所以你会发现此时所占内存大小已经很少了。具体的实现代码public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小 final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); // 调用上面定义的方法计算inSampleSize值 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 使用获取到的inSampleSize值再次解析图片 options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); }思考:inJustDecodeBounds这个参数是干什么的?如果设置为true则表示decode函数不会生成bitmap对象,仅是将图像相关的参数填充到option对象里,这样我们就可以在不生成bitmap而获取到图像的相关参数了。为何设置两次inJustDecodeBounds属性?第一次:设置为true则表示decode函数不会生成bitmap对象,仅是将图像相关的参数填充到option对象里,这样我们就可以在不生成bitmap而获取到图像的相关参数。第二次:将inJustDecodeBounds设置为false再次调用decode函数时就能生成bitmap了。而此时的bitmap已经压缩减小很多了,所以加载到内存中并不会导致OOM。3.3 图片质量压缩在Android中,对图片进行质量压缩,通常我们的实现方式如下所示://quality 为0~100,0表示最小体积,100表示最高质量,对应体积也是最大 bitmap.compress(Bitmap.CompressFormat.JPEG, quality , outputStream);在上述代码中,我们选择的压缩格式是CompressFormat.JPEG,除此之外还有两个选择:其一,CompressFormat.PNG,PNG格式是无损的,它无法再进行质量压缩,quality这个参数就没有作用了,会被忽略,所以最后图片保存成的文件大小不会有变化;其二,CompressFormat.WEBP,这个格式是google推出的图片格式,它会比JPEG更加省空间,经过实测大概可以优化30%左右。Android质量压缩逻辑,函数compress经过一连串的java层调用之后,最后来到了一个native函数:具体看:Bitmap.cpp,最后调用了函数encoder->encodeStream(…)编码保存本地。该函数是调用skia引擎来对图片进行编码压缩。3.4 双线性采样压缩双线性采样(Bilinear Resampling)在 Android 中的使用方式一般有两种:bm = Bitmap.createScaledBitmap(bitmap, bitmap.getWidth()/2, bitmap.getHeight()/2, true); //或者直接使用 matrix 进行缩放 Matrix matrix = new Matrix(); matrix.setScale(0.5f, 0.5f); bm = Bitmap.createBitmap(bitmap, 0, 0, bit.getWidth(), bit.getHeight(), matrix, true);看源码可以知道createScaledBitmap函数最终也是使用第二种方式的matrix进行缩放双线性采样使用的是双线性內插值算法,这个算法不像邻近点插值算法一样,直接粗暴的选择一个像素,而是参考了源像素相应位置周围2x2个点的值,根据相对位置取对应的权重,经过计算之后得到目标图像。3.5 高清图分片加载适用场景 : 当一张图片非常大 , 在手机中只需要显示其中一部分内容 , BitmapRegionDecoder 非常有用 。主要作用 : BitmapRegionDecoder 可以从图像中 解码一个矩形区域 。相当于手在滑动的过程中,计算当前显示区域的图片绘制出来。基本使用流程 : 先创建,后解码 。调用 newInstance 方法 , 创建 BitmapRegionDecoder 对象 ;然后调用 decodeRegion 方法 , 获取指定 Rect 矩形区域的解码后的 Bitmap 对象3.6 图片综合压缩一般情况下图片综合压缩的整体思路如下:第一步进行采样率压缩;第二步进行宽高的等比例压缩(微信对原图和缩略图限制了最大长宽或者最小长宽);第三步就是对图片的质量进行压缩(一般75或者70);第四步就是采用webP的格式。关于图片压缩的综合案例如下具体可以参考:CompressServer04.色彩格式及内存优化4.1 RGB颜色种类RGB 色彩模式是工业界的一种颜色标准通过对红(R)、绿(G)、蓝(B)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色的,RGB即是代表红、绿、蓝三个通道的颜色,这个标准几乎包括了人类视力所能感知的所有颜色,是运用最广的颜色系统之一。Android 中,像素的存储方式使用的色彩模式正是 RGB 色彩模式。4.2 ARGB色彩模式在 Android 中,我们常见的一些颜色设置,都是 RGB 色彩模式来描述像素颜色的,并且他们都带有透明度通道,也就是所谓的 ARGB。例如,我们常见的颜色定义如下://在代码中定义颜色值:蓝色 public final int blue=0xff0000ff; //或者在xml中定义: <drawable name="blue">#ff0000ff</drawable> 以上设置中,颜色值都是使用 16 进制的数字来表示的。以上颜色值都是带有透明度(透明通道)的颜色值,格式是 AARRGGBB,透明度、红色、绿色、蓝色四个颜色通道,各占有 2 位,也就是一个颜色通道,使用了 1 个字节来存储。4.3 改变色彩格式优化Android 中有多种 RGB 模式,我们可以设置不同的格式,来控制图片像素颜色的显示质量和存储空间。Android.graphics.Bitmap 类里有一个内部类 Bitmap.Config 类,它定义了可以在 Android 中使用的几种色彩格式:public enum Config { ALPHA_8 (1), RGB_565 (3), @Deprecated ARGB_4444 (4), ARGB_8888 (5), RGBA_F16 (6), HARDWARE (7); }解释一下这几个值分别代表了什么含义?我们已经知道了:A 代表透明度、R 代表红色、G 代表绿色、B 代表蓝色。ALPHA_8:表示,只存在 Alpha 通道,没有存储色彩值,只含有透明度,每个像素占用 1 个字节的空间。RGB_565:表示,R 占用 5 位二进制的位置,G 占用了6位,B 占用了 5 位。每个像素占用 2 个字节空间,并且不包含透明度。ARGB_4444:表示,A(透明度)、R(红色)、G(绿色)、B(蓝色)4个通道各占用 4 个 bit 位。每个像素占用 2 个字节空间。ARGB_8888:表示,A(透明度)、R(红色)、G(绿色)、B(蓝色)4个通道各占用 8 个 bit 位。每个像素占用 4 个字节空间。RGBA_F16:表示,每个像素存储在8个字节上。此配置特别适合广色域和HDR内容。HARDWARE:特殊配置,当位图仅存储在图形内存中时。 此配置中的位图始终是不可变的。那么开发中一般选择哪一种比较合适呢Android 中的图片在加载时,默认的色彩格式是 ARGB_8888,也就是每个像素占用 4 个字节空间,一张 2700 1900 像素的照片,加载到内存就需要 19.6M 内存空间(2592 1936 * 4 bytes)。如果图片在 UI 组件中显示时,不需要太高的图片质量,例如显示一张缩略图(不透明图片)等场景,这时,我们就没必要使用 ARGB_8888 的色彩格式了,只需要使用 RGB_565 模式即可满足显示的需要。那么,我们的优化操作就可以是:将 2700 1900 像素的原图,压缩到原图的低分辨率的缩略图 270 190 像素的图片,这时需要 0.2M 的内存。也就是从 19.6M内存,压缩为 0.2 M内存。我们还可以进一步优化色彩格式,由 ARGB_8888 改为 RGB_565 模式,这时,目标图片需要的内存就变为 270 190 2 = 0.1M 了。图片内存空间又减小了一倍。05.缓存的使用实践优化5.1 Lru内存缓存LruCache 类特别适合用来缓存 Bitmap,它使用一个强引用的 LinkedHashMap 保存最近引用的对象,并且在缓存超出设定大小时,删除最近最少使用的对象。给 LruCache 确定一个合适的缓存大小非常重要,我们需要考虑几个因素:应用剩余多少可用内存?需要有多少张图片同时显示到屏幕上?有多少图片需要准备好以便马上显示到屏幕?设备的屏幕大小和密度是多少?高密度的设备需要更大的缓存空间来缓存同样数量的图片。Bitmap 的尺寸配置是多少,花费多少内存?图片被访问的频率如何?如果其中一些比另外一些访问更频繁,那么我们可能希望在内存中保存那些最常访问的图片,或者根据访问频率给 Bitmap 分组,为不同的 Bitmap 组设置多个 LruCache 对象。是否可以在缓存图片的质量和数量之间寻找平衡点?有时,保存大量低质量的 Bitmap 会非常有用,加载更高质量的图片的任务可以交给另外一个后台线程处理。缓存太小会导致额外的花销却没有明显的好处,缓存太大同样会导致 java.lang.OutOfMemory 的异常,并且使得你的程序只留下小部分的内存用来工作(缓存占用太多内存,导致其他操作会因为内存不够而抛出异常)。所以,我们需要分析实际情况之后,提出一个合适的解决方案。LruCache是Android提供的一个缓存类,通常运用于内存缓存LruCache是一个泛型类,它的底层是用一个LinkedHashMap以强引用的方式存储外界的缓存对象来实现的。为什么使用LinkedHashMap来作为LruCache的存储,是因为LinkedHashMap有两种排序方式,一种是插入排序方式,一种是访问排序方式,默认情况下是以访问方式来存储缓存对象的;LruCache提供了get和put方法来完成缓存的获取和添加,当缓存满时,会将最近最少使用的对象移除掉,然后再添加新的缓存对象。如下源码所示,底层是LinkedHashMap。public LruCache(int maxSize) { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } this.maxSize = maxSize; this.map = new LinkedHashMap<K, V>(0, 0.75f, true); }在使用LruCache的时候,首先需要获取当前设备的内存容量,通常情况下会将总容量的八分之一作为LruCache的容量,然后重写LruCache的sizeof方法,sizeof方法用于计算缓存对象的大小,单位需要与分配的容量的单位一致;// 获取系统最大缓存 int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // set LruCache size; // 使用最大可用内存值的1/8作为缓存的大小 int cacheSize = maxMemory / 8; LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(@NonNull String uri, @NonNull Bitmap bitmap) { // 重写此方法来衡量每张图片的大小,默认返回图片数量 return bitmap.getRowBytes() * bitmap.getHeight() / 1024; } }; //插入对象 memoryCache.put(key, bitmap); //取出对象 memoryCache.get(key);如何淘汰缓存这个就要看LinkedHashMap集合的特点呢!LinkedHashMap 构造函数的第三个参数:accessOrder,传入true时, 元素会按访问顺序排列,最后访问的在遍历器最后端。在进行淘汰时,移除遍历器前端的元素,直至缓存总大小降低到指定大小以下。5.2 Lru内存注意事项看一个真实的场景假设我们的LruCache可以缓存80张,每次刷新从网络获取20张图片且不重复,那么在刷新第五次的时候,根据LruCache缓存的规则,第一次刷新的20张图片就会从LruCache中移出,处于等待被系统GC的状态。如果我们继续刷新n次,等待被回收的张数就会累积到 20 * n 张。会出现什么问题会出现大量的Bitmap内存碎片,我们不知道系统什么时候会触发GC回收掉这些无用的Bitmap,对于内存是否会溢出,是否会频繁GC导致卡顿等未知问题。解决方案该怎么做?第一种:在3.0以后引入了 BitmapFactory.Options.inBitmap,如果设置此项,需要解码的图片就会尝试使用该Bitmap的内存,这样取消了内存的动态分配,提高了性能,节省了内存。第二种:把处于无用的状态的Bitmap放入SoftReference。SoftReference引用的对象会在内存溢出之前被回收。关于Lru缓存案例和代码可以参考:AppLruCache5.3 使用Lru磁盘缓存内存缓存能够提高访问最近用过的 Bitmap 的速度,但是我们无法保证最近访问过的 Bitmap 都能够保存在缓存中。像类似 GridView 等需要大量数据填充的控件很容易就会用尽整个内存缓存。另外,我们的应用可能会被类似打电话等行为而暂停并退到后台,因为后台应用可能会被杀死,那么内存缓存就会被销毁,里面的 Bitmap 也就不存在了。一旦用户恢复应用的状态,那么应用就需要重新处理那些图片。磁盘缓存可以用来保存那些已经处理过的 Bitmap,它还可以减少那些不再内存缓存中的 Bitmap 的加载次数。当然从磁盘读取图片会比从内存要慢,而且由于磁盘读取操作时间是不可预期的,读取操作需要在后台线程中处理。注意:如果图片会被更频繁的访问,使用 ContentProvider 或许会更加合适,比如在图库应用中。注意:因为初始化磁盘缓存涉及到 I/O 操作,所以它不应该在主线程中进行。但是这也意味着在初始化完成之前缓存可以被访问。为了解决这个问题,在上面的实现中,有一个锁对象(lock object)来确保在磁盘缓存完成初始化之前,应用无法对它进行读取。内存缓存的检查是可以在 UI 线程中进行的,磁盘缓存的检查需要在后台线程中处理。磁盘操作永远都不应该在 UI 线程中发生。当图片处理完成后,Bitmap 需要添加到内存缓存与磁盘缓存中,方便之后的使用。06.不同版本对Bitmap管理6.1 演变进程Android 2.3.3 (API level 10)以及之前,一个 Bitmap 的像素数据是存放在 Native 内存空间中的。这些数据与 Bitmap 对象本身是隔离的,Bitmap 本身被存放在 Dalvik 堆中。并且无法预测在 Native 内存中的像素级数据何时会被释放,这意味着程序容易超过它的内存限制并且崩溃。Android 3.0 (API Level 11)开始像素数据则是与 Bitmap 本身一起存放在 Dalvik 堆中。Android 8.0(Android O)及之后的版本中Bitmap 的像素数据的内存分配又回到了 Native 层,它是在 Native 堆空间进行分配的。6.2 管理Bitmap内存管理 Android 2.3.3 及以下版本的内存使用在 Android 2.3.3 (API level 10) 以及更低版本上,推荐使用 recycle() 方法。 如果在应用中显示了大量的 Bitmap 数据,我们很可能会遇到 OutOfMemoryError 的错误。 recycle() 方法可以使得程序更快的释放内存。管理 Android 3.0 及其以上版本的内存从 Android 3.0 (API Level 11)开始,引进了 BitmapFactory.Options.inBitmap 字段。 如果使用了这个设置字段,decode 方法会在加载 Bitmap 数据的时候去重用已经存在的 Bitmap。这意味着 Bitmap 的内存是被重新利用的,这样可以提升性能,并且减少了内存的分配与回收。然而,使用 inBitmap 有一些限制,特别是在Android 4.4 (API level 19)之前,只有同等大小的位图才可以被重用。管理 Android 8.0 及其以上版本的内存在 Android 8.0 及其以上版本,处理内存,也遵循 Android 3.0 以上版本同样的方式。同时,图片像素数据存储在 native 层,并且不占用 Java 堆的空间,这也代表着我们拥有更大的图片存储空间,可以加载质量更高、数据更多的图片到内存中。但是,内存依然不是无限的,应用还是要受到手机内存的限制,所以一定要注意这一点。6.3 提高Bitmap复用Android3.0之后,并没有强调Bitmap.recycle();而是强调Bitmap的复用。使用LruCache对Bitmap进行缓存,当再次使用到这个Bitmap的时候直接获取,而不用重走编码流程。Android3.0(API 11之后)引入了BitmapFactory.Options.inBitmap字段,设置此字段之后解码方法会尝试复用一张存在的Bitmap。这意味着Bitmap的内存被复用,避免了内存的回收及申请过程,显然性能表现更佳。使用这个字段有几点限制:声明可被复用的Bitmap必须设置inMutable为true;Android4.4(API 19)之前只有格式为jpg、png,同等宽高(要求苛刻),inSampleSize为1的Bitmap才可以复用;Android4.4(API 19)之前被复用的Bitmap的inPreferredConfig会覆盖待分配内存的Bitmap设置的inPreferredConfig;Android4.4(API 19)之后被复用的Bitmap的内存必须大于需要申请内存的Bitmap的内存;Android4.4(API 19)之前待加载Bitmap的Options.inSampleSize必须明确指定为1。Bitmap复用的实验,代码如下所示,然后看打印的日志信息从内存地址的打印可以看出,两个对象其实是一个对象,Bitmap复用成功;bitmapReuse占用的内存(4346880)正好是bitmap占用内存(1228800)的四分之一;getByteCount()获取到的是当前图片应当所占内存大小,getAllocationByteCount()获取到的是被复用Bitmap真实占用内存大小。虽然bitmapReuse的内存只有4346880,但是因为是复用的bitmap的内存,因而其真实占用的内存大小是被复用的bitmap的内存大小(1228800)。这也是getAllocationByteCount()可能比getByteCount()大的原因。@RequiresApi(api = Build.VERSION_CODES.KITKAT) private void initBitmap() { BitmapFactory.Options options = new BitmapFactory.Options(); // 图片复用,这个属性必须设置; options.inMutable = true; // 手动设置缩放比例,使其取整数,方便计算、观察数据; options.inDensity = 320; options.inTargetDensity = 320; Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bg_autumn_tree_min, options); // 对象内存地址; Log.i("ycBitmap", "bitmap = " + bitmap); Log.i("ycBitmap", "ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount()); // 使用inBitmap属性,这个属性必须设置; options.inBitmap = bitmap; options.inDensity = 320; // 设置缩放宽高为原始宽高一半; options.inTargetDensity = 160; options.inMutable = true; Bitmap bitmapReuse = BitmapFactory.decodeResource(getResources(), R.drawable.bg_kites_min, options); // 复用对象的内存地址; Log.i("ycBitmap", "bitmapReuse = " + bitmapReuse); Log.i("ycBitmap", "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount()); Log.i("ycBitmap", "bitmapReuse:ByteCount = " + bitmapReuse.getByteCount() + ":::bitmapReuse:AllocationByteCount = " + bitmapReuse.getAllocationByteCount()); //11-26 18:24:07.971 15470-15470/com.yc.ycbanner I/ycBitmap: bitmap = android.graphics.Bitmap@9739bff //11-26 18:24:07.972 15470-15470/com.yc.ycbanner I/ycBitmap: bitmap:ByteCount = 4346880:::bitmap:AllocationByteCount = 4346880 //11-26 18:24:07.994 15470-15470/com.yc.ycbanner I/ycBitmap: bitmapReuse = android.graphics.Bitmap@9739bff //11-26 18:24:07.994 15470-15470/com.yc.ycbanner I/ycBitmap: bitmap:ByteCount = 1228800:::bitmap:AllocationByteCount = 4346880 //11-26 18:24:07.994 15470-15470/com.yc.ycbanner I/ycBitmap: bitmapReuse:ByteCount = 1228800:::bitmapReuse:AllocationByteCount = 4346880 }07.图片其他方面优化7.1 减少PNG图片的使用这里要介绍一种新的图片格式:Webp,它是由 Google 推出的一种既保留 png 格式的优点,又能够减少图片大小的一种新型图片格式。在 Android 4.0(API level 14) 中支持有损的 WebP 图像,在 Android 4.3(API level 18) 和更高版本中支持无损和透明的 WebP 图像。注意一下,Webp格式图片仅仅只是减少图片的质量大小,并不会减少加载图片后的内存占用。7.2 切割圆角优化方案1:直接采用Canvas.clipPath 相关api,裁剪出一个圆角区域。该方案简单暴力,通用性强。如果只是一个静态的单图视图,该方法问题不大,但如果是复杂页面,滚动的时候,测试就会跟你说,页面卡顿了,要优化。原因就是 Canvas.clip的相关api损耗相对较大。方案2:系统提供的CardView设置圆角把原来全工程各个视频控件和图片控件的外层,都加上一层CardView。改造成本大,布局层级更深一层,layout时间加长。方案3:使用setXfermode法此种方式就是再new一个相同尺寸的bitmap,然后使用paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));先画圆角矩形,再画原始bitmap,然后就得到了一个圆角的bitmap。早期用得较多,占用bitmap双倍内存。方案4:图片加载库比如Glide,Fresco等在底层,无非也是使用上面的这两种种方式。早期的使用setXfermode来实现,后来使用BitmapShader实现。使用简单,稳定。方案5:遮罩还是使用setXfermode,不过与方法一不同的是:不对图片作任何更改,只在圆角之外再画一层与背景颜色相同的四个角来遮挡,在视觉上造成圆角图片的效果。那个切割圆角该怎么优化呢?使用方案3,可以采用自定义view,支持LinearLayout、RelativeLayout、FrameLayout、ConstraintLayout、ImageView、TextView、View、Button等设置圆角。具体案例可见:RoundCorners7.3 如何给图片置灰色大概的操作步骤。具体可以参考:PicCalculateUtils第一步:获取原始图片的宽高,然后创建一个bitmap可变位图对象。第二步:创建画板canvas对象,然后创建画笔paint。然后调用canvas.drawBitmap方法绘制图片第三步:对画笔进行修饰,设置画笔颜色属性,这里使用到了ColorMatrix,核心就是设置饱和度为0,即可绘制灰色内容7.4 如何处理图片旋转呢在Android中使用ImageView显示图片的时候发现图片显示不正,方向偏了或者倒过来了。解决这个问题很自然想到的两步走,首先是要自动识别图像方向,计算旋转角度,然后对图像进行旋转并显示。识别图像方向首先在这里提一个概念EXIF(Exchangeable Image File Format,可交换图像文件)。简而言之,Exif是一个标准,用于电子照相机(也包括手机、扫描器等)上,用来规范图片、声音、视屏以及它们的一些辅助标记格式。Exif支持的格式如下:图像;压缩图像文件:JPEG、DCT;非压缩图像文件:TIFF;音频;RIFF、WAVAndroid提供了对JPEG格式图像Exif接口支持,可以读取JPEG文件metadata信息,参见ExifInterface。这些Metadata信息总的来说大致分为三类:日期时间、空间信息(经纬度、高度)、Camera信息(孔径、焦距、旋转角、曝光量等等)。关于图像旋转获取了图片的旋转方向后,然后再设置图像旋转。最后Bitmap提供的静态createBitmap方法,可以对图片设置旋转角度。具体看:PicCalculateUtils7.5 保存图片且刷相册大概的操作步骤如下所示。具体可看:ImageSaveUtils第一步:创建图片文件,然后将bitmap对象写到图片文件中第二步:通过MediaStore将图片插入到共享目录相册中第三步:发送通知,通知相册中刷新插入图片的数据。注意,获取图片资源uri刷新即可,避免刷新所有数据造成等待时间过长。7.6 统一图片域名优化域名统一减少了10%+的重复图片下载和内存消耗。同时减少之前多域名图片加载时重复创建HTTPS请求的过程,减少图片加载时间。7.7 优化H5图片加载通过拦截WebView图片加载的方式,让原生图片库来下载图片之后传递图片二进制数据给WebView显示。采用OkHttp拦截资源缓存,下面是大概的思路。缓存的入口从shouldInterceptRequest出发第一步,拿到WebResourceRequest对象中请求资源的url还有header,如果开发者设置不缓存则返回null第二步,如果缓存,通过url判断拦截资源的条件,过滤非http,音视频等资源,这个是可自由配置缓存内容比如css,png,jpg,xml,txt等第三步,判断本地是否有OkHttp缓存数据,如果有则直接读取本地资源,通过url找到对应的path路径,然后读取文件流,组装数据返回。第四步,如果没有缓存数据,创建OkHttp的Request请求,将资源网络请求交给okHttp来处理,并且用它自带的缓存功能,当然如果是请求失败或者异常则返回null,否则返回正常数据关于webView图片缓存的方案,可以直接参考:YCWebView7.8 优化图片阴影效果阴影效果有哪些实现方式第一种:使用CardView,但是不能设置阴影颜色第二种:采用shape叠加,存在后期UI效果不便优化第三种:UI切图第四种:自定义View第五种:自定义Drawable否定上面前两种方案原因分析?第一个方案的CardView渐变色和阴影效果很难控制,只能支持线性或者环装形式渐变,这种不满足需要,因为阴影本身是一个四周一层很淡的颜色包围,在一个矩形框的层面上颜色大概一致,而且这个CardView有很多局限性,比如不能修改阴影的颜色,不能修改阴影的深浅。所以这个思路无法实现这个需求。第二个采用shape叠加,可以实现阴影效果,但是影响UI,且阴影部分是占像素的,而且不灵活。第三个方案询问了一下ui。他们给出的结果是如果使用切图的话那标注的话很难标,身为一个优秀的设计师大多对像素点都和敏感,界面上的像素点有一点不协调那都是无法容忍的。网上一些介绍阴影效果方案所有在深奥的技术,也都是为需求做准备的。也就是需要实践并且可以用到实际开发中,这篇文章不再抽象介绍阴影效果原理,理解三维空间中如何处理偏移光线达到阴影视差等,网上看了一些文章也没看明白或者理解。这篇博客直接通过调用api实现预期的效果。多个drawable叠加,使用layer-list可以将多个drawable按照顺序层叠在一起显示,默认情况下,所有的item中的drawable都会自动根据它附上view的大小而进行缩放,layer-list中的item是按照顺序从下往上叠加的,即先定义的item在下面,后面的依次往上面叠放阴影是否占位使用CardView阴影不占位,不能设置阴影颜色和效果使用shape阴影是可以设置阴影颜色,但是是占位的几种方案优缺点对比分析CardView 优点:自带功能实现简单 缺点:自带圆角不一定可适配所有需求layer(shape叠加) 优点:实现形式简单 缺点:效果一般自定义实现 优点:实现效果好可配置能力高 缺点:需要开发者自行开发关于解决阴影效果具体各种方案的对比可以参考这个demo:AppShadowLib7.9 图片资源的压缩我们应用中使用的图片,设计师出的原图通常都非常大,他们通常会使用工具,经过一定的压缩,缩减到比较小一些的大小。但是,这些图片通常都有一定的可压缩空间,我在之前的项目中,对图片进行了二次压缩,整体压缩率达到了 40%~50% ,效果还是非常不错的。这里介绍下常用的,图片压缩的方法:使用压缩工具对图片进行二次压缩。根据最终图片是否需要透明度展示,优先选择不透明的图片格式,例如,我们应该避免使用 png 格式的图片。对于色彩简单,例如,一些背景之类的图片,可以选择使用布局文件来定义(矢量图),这样就会非常节省内存了。如果包含透明度,优先使用 WebP 等格式图像。图片在上线前进行压缩处理,不但可以减少内存的使用,如果图片是网络获取的,也可以减少网络加载的流量和时间。推荐一个图片压缩网站:tinypng网站
文章
2022-10-16
Android的GridView滚动速度计算
怎么计算出Android的GridView的滚动速度,用于GridView加载网络图片,当滚动过快就不加载图片,滚动慢就加载图片
问答
Android开发
2016-06-07
Android的GridView滚动速度计算
怎么计算出Android的GridView的滚动速度,用于GridView加载网络图片,当滚动过快就不加载图片,滚动慢就加载图片
问答
Android开发
2016-06-03
Flutter开发常用第三方插件持续更新
2020.5.25更新权限处理permission_handler录音audio_recorder声音播放audioplayers路由管理框架fluro2020.5.20整理Bilibili开源的视频播放组件,fijkplayr 是基于 ijkplayer 封装的 flutter 媒体播放器,开箱即用,无需编译 ijkplayerfijkplayer加载动画库flutter_spinkit网络请求库,dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等...Dio图片缓存框架cached_network_image轮播组件,flutter最强大的siwiper, 多种布局方式,无限轮播,Android和IOS双端适配.flutter_swiperAzListView,Flutter 城市列表,联系人列表,自定义Header,索引,悬停效果。azlistview事件总线/消息监听event_bus图标icon库cupertino_iconslpinyin (汉字转拼音Flutter版)lpinyin获取App包信息插件package_infoFlutter版的GridView网格布局flutter_staggered_grid_view启动白屏处理框架flutter_splash_screen相册取图/拍照框架image_pickerFlutter上拉刷新,下拉加载框架pull_to_refresh
文章
缓存  ·  Dart  ·  API  ·  Android开发  ·  iOS开发  ·  索引
2022-06-10
阿里 移动端(Android)编码规范
为指导 Android 开发者更加高效、高质量地进行 App 开发,呈现给用户体验好、性能优、稳定性佳、安全性高的产品。本手册以开发者为中心视角分为Java语言规范,Android 资源文件命名与使用,Android 基本组件,UI 与布局,进程、线程与消息通信,文件与数据库,Bitmap、Drawable 与动画,安全,其他等九大部分,根据约束力强弱,规约依次分为强制、推荐、参考三大类:【强制】必须遵守,违反本约定或将会引起严重的后果;【推荐】尽量遵守,长期遵守有助于系统稳定性和合作效率的提升;【参考】充分理解,技术意识的引导,是个人学习、团队沟通、项目合作的方向一、Java 语言规范遵循Oracle公司《The Java Language Specification, Java SE 8 Edition》二、Android 资源文件命名与使用【推荐】资源文件需带模块前缀。【推荐】layout 文件的命名方式。Activity 的 layout 以 module_activity 开头 Fragment 的 layout 以 module_fragment 开头 Dialog 的 layout 以 module_dialog 开头 include 的 layout 以 module_include 开头 ListView 的行 layout 以 module_list_item 开头 RecyclerView 的 item layout 以 module_recycle_item 开头 GridView 的行 layout 以 module_grid_item 开头【推荐】 drawable 资源名称以小写单词+下划线的方式命名,根据分辨率不同存放在不同的 drawable 目录下,建议只使用一套,例如 drawable-xhdpi。采用规则如下: 模块名_业务功能描述_控件描述_控件状态限定词如:module_login_btn_pressed,module_tabs_icon_home_normal【推荐】anim 资源名称以小写单词+下划线的方式命名,采用以下规则:模块名_逻辑名称_[方向|序号]tween 动 画 资 源 : 尽 可 能 以 通 用 的 动 画 名 称 命 名 , 如 module_fade_in, module_fade_out, module_push_down_in (动画+方向);frame 动画资源:尽可能以模 块+功能命名+序号。如:module_loading_grey_001【推荐】 color 资源使用#AARRGGBB 格式,写入 module_colors.xml 文件中,命名格式采用以下规则:模块名_逻辑名称_颜色如:<color name="module_btn_bg_color">#33b5e5e5</color>【推荐】dimen 资源以小写单词+下划线方式命名,写入 module_dimens.xml 文件中,采用以下规则:模块名_描述信息如:<dimen name="module_horizontal_line_height">1dp</dimen>【推荐】style 资源采用小写单词+下划线方式命名,写入 module_styles.xml 文件中,采用以下规则:父 style 名称.当前 style 名称如:<style name="ParentTheme.ThisActivityTheme"> … </style>【推荐】string资源文件或者文本用到字符需要全部写入module_strings.xml文件中,字符串以小写单词+下划线的方式命名,采用以下规则:模块名_逻辑名称如:moudule_login_tips,module_homepage_notice_desc【推荐】Id 资源原则上以驼峰法命名,View 组件的资源 id 需要以 View 的缩写作为前缀。常用缩写表如下:控件缩写LinearLayoutllRelativeLayoutrlConstraintLayoutclListViewlvScollViewsvTextViewtvButtonbtnImageViewivCheckBoxcbRadioButtonrbEditTextet其它控件的缩写推荐使用小写字母并用下划线进行分割,例如:ProgressBar 对应的缩写为 progress_barDatePicker 对应的缩写为 date_picker10.【推荐】大分辨率图片(单维度超过 1000)大分辨率图片建议统一放在 xxhdpi 目录下管理,否则将导致占用内存成倍数增加。三、Android 基本组件Android 基本组件指 Activity、Fragment、Service、BroadcastReceiver、ContentProvider 等等。【强制】Activity 间的数据通信,对于数据量比较大的,避免使用 Intent + Parcelable的方式,可以考虑 EventBus 等替代方案,以免造成 TransactionTooLargeException。【推荐】Activity#onSaveInstanceState()方法不是 Activity 生命周期方法,也不保证一定会被调用。它是用来在 Activity 被意外销毁时保存 UI 状态的,只能用于保存临时性数据,例如 UI 控件的属性等,不能跟数据的持久化存储混为一谈。持久化存储应该在 Activity#onPause()/onStop()中实行。【强制】Activity 间通过隐式 Intent 的跳转,在发出 Intent 之前必须通过 resolveActivity检查,避免找不到合适的调用组件,造成ActivityNotFoundException 的异常。正例:public void viewUrl(String url, String mimeType) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(Uri.parse(url), mimeType); if (getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ ONLY) != null) { try { startActivity(intent); } catch (ActivityNotFoundException e) { if (Config.LOGD) { Log.d(LOGTAG, "activity not found for " + mimeType + " over " + Uri.parse(url). getScheme(), e); } } } }反例:Intent intent = new Intent(); intent.setAction("com.great.activity_intent.Intent_Demo1_Result3");【强制】避免在 Service#onStartCommand()/onBind()方法中执行耗时操作,如果确实有需求,应改用 IntentService 或采用其他异步机制完成。【强制】避免在 BroadcastReceiver#onReceive()中执行耗时操作,如果有耗时工作,应该创建 IntentService 完成,而不应该在 BroadcastReceiver 内创建子线程去做。【强制】避免使用隐式 Intent 广播敏感信息,信息可能被其他注册了对应BroadcastReceiver 的 App 接收。【 推 荐 】 添 加 Fragment 时 , 确 保 FragmentTransaction#commit() 在Activity#onPostResume()或者 FragmentActivity#onResumeFragments()内调用。不要随意使用 FragmentTransaction#commitAllowingStateLoss()来代替,任何commitAllowingStateLoss()的使用必须经过 code review,确保无负面影响。【推荐】不要在 Activity#onDestroy()内执行释放资源的工作,例如一些工作线程的销毁和停止,因为 onDestroy()执行的时机可能较晚。可根据实际需要,在Activity#onPause()/onStop()中结合 isFinishing()的判断来执行。【推荐】如非必须,避免使用嵌套的 Fragment。【推荐】总是使用显式 Intent 启动或者绑定 Service,且不要为服务声明 Intent Filter,保证应用的安全性。如果确实需要使用隐式调用,则可为 Service 提供 Intent Filter并从 Intent 中排除相应的组件名称,但必须搭配使用 Intent#setPackage()方法设置Intent 的指定包名,这样可以充分消除目标服务的不确定性。11.【推荐】Service 需要以多线程来并发处理多个启动请求,建议使用 IntentService,可避免各种复杂的设置。12.【推荐】对于只用于应用内的广播,优先使用 LocalBroadcastManager 来进行注册和发送,LocalBroadcastManager 安全性更好,同时拥有更高的运行效率。【推荐】当前Activity的onPause方法执行结束后才会执行下一个Activity的onCreate方法,所以在 onPause 方法中不适合做耗时较长的工作,这会影响到页面之间的跳转效率。14.【强制】不要在 Android 的 Application 对象中缓存数据。基础组件之间的数据共享请使用 Intent 等机制,也可使用 SharedPreferences 等数据持久化机制。15.【推荐】使用 Toast 时,建议定义一个全局的 Toast 对象,这样可以避免连续显示Toast 时不能取消上一次 Toast 消息的情况(如果你有连续弹出 Toast 的情况,避免使用 Toast.makeText)。16.【强制】使用 Adapter 的时候,如果你使用了 ViewHolder 做缓存,在 getView()的方法中无论这项 convertView 的每个子控件是否需要设置属性(比如某个 TextView设置的文本可能为 null,某个按钮的背景色为透明,某控件的颜色为透明等),都需要为其显式设置属性(Textview 的文本为空也需要设置 setText(""),背景透明也需要设置),否则在滑动的过程中,因为 adapter item 复用的原因,会出现内容的显示错乱。【强制】Activity或者Fragment中动态注册BroadCastReceiver时,registerReceiver()和 unregisterReceiver()要成对出现。四、UI 与布局【强制】布局中不得不使用 ViewGroup 多重嵌套时,不要使用 LinearLayout 嵌套,改用 RelativeLayout,可以有效降低嵌套数。【推荐】在 Activity 中显示对话框或弹出浮层时,尽量使用 DialogFragment,而非Dialog/AlertDialog,这样便于随Activity生命周期管理对话框/弹出浮层的生命周期。正例:public void showPromptDialog(String text){ DialogFragment promptDialog = new DialogFragment() { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE); View view = inflater.inflate(R.layout.fragment_prompt, container); return view; } }; promptDialog.show(getFragmentManager(), text);【推荐】源文件统一采用 UTF-8 的形式进行编码。【强制】禁止在非 ui 线程进行 view 相关操作。【推荐】文本大小使用单位 dp,view 大小使用单位 dp。对于 Textview,如果在文字大小确定的情况下推荐使用 wrap_content 布局避免出现文字显示不全的适配问题。【强制】禁止在设计布局时多次设置子 view 和父 view 中为同样的背景造成页面过度绘制,推荐将不需要显示的布局进行及时隐藏。【推荐】灵活使用布局,推荐 Merge、ViewStub 来优化布局,尽可能多的减少 UI布局层级,推荐使用 FrameLayout,LinearLayout、RelativeLayout 次之。【推荐】在需要时刻刷新某一区域的组件时,建议通过以下方式避免引发全局 layout刷新:设置固定的 view 大小的高宽,如倒计时组件等;调用 view 的 layout 方式修改位置,如弹幕组件等;通过修改 canvas 位置并且调用 invalidate(int l, int t, int r, int b)等方式限定刷新区域;通过设置一个是否允许 requestLayout 的变量,然后重写控件的 requestlayout、onSizeChanged 方法 , 判 断 控 件 的大小 没 有 改 变 的 情况下 , 当 进 入requestLayout 的时候,直接返回而不调用 super 的 requestLayout 方法。【推荐】不能在 Activity 没有完全显示时显示 PopupWindow 和 Dialog。11.【推荐】尽量不要使用 AnimationDrawable,它在初始化的时候就将所有图片加载到内存中,特别占内存,并且还不能释放,释放之后下次进入再次加载时会报错。12.【强制】不能使用 ScrollView 包裹 ListView/GridView/ExpandableListVIew;因为这样会把 ListView 的所有 Item 都加载到内存中,要消耗巨大的内存和 cpu 去绘制图面。五、进程、线程与消息通信【强制】不要通过 Intent 在 Android 基础组件之间传递大数据(binder transaction缓存为 1MB),可能导致 OOM。【强制】在 Application 的业务初始化代码加入进程判断,确保只在自己需要的进程初始化。特别是后台进程减少不必要的业务初始化。【强制】新建线程时,必须通过线程池提供(AsyncTask 或者 ThreadPoolExecutor 或者其他形式自定义的线程池),不允许在应用中自行显式创建线程。【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。【强制】子线程中不能更新界面,更新界面必须在主线程中进行,网络操作不能在主线程中调用。【强制】不要在非 UI 线程中初始化 ViewStub,否则会返回 null。【推荐】尽量减少不同 APP 之间的进程间通信及拉起行为。拉起导致占用系统资源,影响用户体验。【推荐】新建线程时,定义能识别自己业务的线程名称,便于性能优化和问题排查。【推荐】ThreadPoolExecutor 设置线程存活时间(setKeepAliveTime),确保空闲时线程能被释放。【 推荐 】 禁 止 在多 进 程 之 间 用 SharedPreferences 共 享数 据 , 虽 然 可 以(MODE_MULTI_PROCESS),但官方已不推荐。11.【推荐】谨慎使用 Android 的多进程,多进程虽然能够降低主进程的内存压力,但会遇到如下问题:不能实现完全退出所有 Activity 的功能;首次进入新启动进程的页面时会有延时的现象(有可能黑屏、白屏几秒,是白屏还是黑屏和新 Activity 的主题有关);应用内多进程时,Application 实例化多次,需要考虑各个模块是否都需要在所有进程中初始化;多进程间通过 SharedPreferences 共享数据时不稳定。六、文件与数据库【强制】任何时候不要硬编码文件路径,请使用 Android 文件系统 API 访问。【强制】当使用外部存储时,必须检查外部存储的可用性。【强制】应用间共享文件时,不要通过放宽文件系统权限的方式去实现,而应使用FileProvider【推荐】SharedPreference 中只能存储简单数据类型(int、boolean、String 等),复杂数据类型建议使用文件、数据库等其他方式存储。【 推 荐 】 SharedPreference 提 交 数 据 时 , 尽 量 使 用 Editor#apply() , 而 非Editor#commit()。一般来讲,仅当需要确定提交结果,并据此有后续操作时,才使用 Editor#commit()。【强制】数据库 Cursor 必须确保使用完后关闭,以免内存泄漏。【强制】多线程操作写入数据库时,需要使用事务,以免出现同步问题。【推荐】大数据写入数据库时,请使用事务或其他能够提高 I/O 效率的机制,保证执行速度【强制】执行 SQL 语句时,应使用 SQLiteDatabase#insert()、update()、delete(),不要使用 SQLiteDatabase#execSQL(),以免 SQL 注入风险。10.【强制】如果 ContentProvider 管理的数据存储在 SQL 数据库中,应该避免将不受信任的外部数据直接拼接在原始 SQL 语句中,可使用一个用于将 ? 作为可替换参数的选择子句以及一个单独的选择参数数组,会避免 SQL 注入。正例:// 使用一个可替换参数 String mSelectionClause = "var = ?"; String[] selectionArgs = {""}; selectionArgs[0] = mUserInput;反例:// 拼接用户输入内容和列名 String mSelectionClause = "var = " + mUserInput;七、Bitmap、Drawable 与动画【强制】加载大图片或者一次性加载多张图片,应该在异步线程中进行。图片的加载,涉及到 IO 操作,以及 CPU 密集操作,很可能引起卡顿。【强制】在 ListView,ViewPager,RecyclerView,GirdView 等组件中使用图片时,应做好图片的缓存,避免始终持有图片导致内存泄露,也避免重复创建图片,引起性 能 问 题 。 建 议 使 用 Fresco ( https://github.com/facebook/fresco )、 Glide(https://github.com/bumptech/glide)等图片库。【强制】png 图片使用 tinypng 或者类似工具压缩处理,减少包体积。【推荐】应根据实际展示需要,压缩图片,而不是直接显示原图。手机屏幕比较小,直接显示原图,并不会增加视觉上的收益,但是却会耗费大量宝贵的内存。【强制】使用完毕的图片,应该及时回收,释放宝贵的内存。【推荐】针对不同的屏幕密度,提供对应的图片资源,使内存占用和显示效果达到合理的平衡。如果为了节省包体积,可以在不影响 UI 效果的前提下,省略低密度图片。【强制】在 Activity.onPause()或 Activity.onStop()回调中,关闭当前 activity 正在执行的的动画。【推荐】在动画或者其他异步任务结束时,应该考虑回调时刻的环境是否还支持业务处理。例如 Activity 的 onStop()函数已经执行,且在该函数中主动释放了资源,此时回调中如果不做判断就会空指针崩溃。【推荐】使用 inBitmap 重复利用内存空间,避免重复开辟新内存。10.【推荐】使用 ARGB_565 代替 ARGB_888,在不怎么降低视觉效果的前提下,减少内存占用。11.【推荐】尽量减少 Bitmap (BitmapDrawable)的使用,尽量使用纯色(ColorDrawable)、渐变色(GradientDrawable)、StateSelector(StateListDrawable)等与 Shape 结合的形式构建绘图。12.【推荐】谨慎使用 gif 图片,注意限制每个页面允许同时播放的 gif 图片,以及单个gif 图片的大小。13.【参考】大图片资源不要直接打包到 apk,可以考虑通过文件仓库远程下载,减小包体积。14.【推荐】根据设备性能,选择性开启复杂动画,以实现一个整体较优的性能和体验;15.【推荐】在有强依赖 onAnimationEnd 回调的交互时,如动画播放完毕才能操作页面 , onAnimationEnd 可 能 会 因 各 种 异 常 没 被 回 调 ( 参 考 :https://stackoverflow.com/questions/5474923/onanimationend-is-not-getting-called-onanimationstart-works-fine),建议加上超时保护或通过 postDelay 替代onAnimationEnd。【推荐】当 View Animation 执行结束时,调用 View.clearAnimation()释放相关资源。八、安全【强制】使用 PendingIntent 时,禁止使用空 intent,同时禁止使用隐式 Intent。【强制】禁止使用常量初始化矢量参数构建 IvParameterSpec,建议 IV 通过随机方式产生。【强制】将 android:allowbackup 属性设置为 false,防止 adb backup 导出数据。说明:在 AndroidManifest.xml 文件中为了方便对程序数据的备份和恢复在 Android APIlevel 8 以后增加了 android:allowBackup 属性值。默认情况下这个属性值为 true,故当 allowBackup 标志值为 true 时,即可通过 adb backup 和 adb restore 来备份和恢复应用程序数据。正例:<application android:allowBackup="false" android:icon="@drawable/test_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" >【强制】在实现的 HostnameVerifier 子类中,需要使用 verify 函数效验服务器主机名的合法性,否则会导致恶意程序利用中间人攻击绕过主机名效验。【强制】利用 X509TrustManager 子类中的 checkServerTrusted 函数效验服务器端证书的合法性。【强制】META-INF 目录中不能包含如.apk,.odex,.so 等敏感文件,该文件夹没有经过签名,容易被恶意替换。【强制】Receiver/Provider 不能在毫无权限控制的情况下,将 android:export 设置为 true。【参考】数据存储在 Sqlite 或者轻量级存储需要对数据进行加密,取出来的时候进行解密。【强制】阻止 webview 通过 file:schema 方式访问本地敏感数据。10.【强制】不要广播敏感信息,只能在本应用使用 LocalBroadcast,避免被别的应用收到,或者 setPackage 做限制。11.【强制】不要把敏感信息打印到 log 中。说明:在 APP 的开发过程中,为了方便调试,通常会使用 log 函数输出一些关键流程的信息,这些信息中通常会包含敏感内容,如执行流程、明文的用户名密码等,这会让攻击者更加容易的了解 APP 内部结构方便破解和攻击,甚至直接获取到有价值的敏感信息。12.【强制】对于内部使用的组件,显示设置组件的"android:exported"属性为 false13.【强制】应用发布前确保 android:debuggable 属性设置为 false。14.【强制】使用 Intent Scheme URL 需要做过滤。15.【强制】密钥加密存储或者经过变形处理后用于加解密运算,切勿硬编码到代码中。说明:应用程序在加解密时,使用硬编码在程序中的密钥,攻击者通过反编译拿到密钥可以轻易解密 APP 通信数据。16.【强制】将所需要动态加载的文件放置在 apk 内部,或应用私有目录中,如果应用必须要把所加载的文件放置在可被其他应用读写的目录中(比如 sdcard),建议对不可信的加载源进行完整性校验和白名单处理,以保证不被恶意代码注入。17.【强制】除非 min API level >=17,请注意 addJavascriptInterface 的使用。18.【强制】使用 Android 的 AES/DES/DESede 加密算法时,不要使用默认的加密模式ECB,应显示指定使用 CBC 或 CFB 加密模式。19.【强制】不要使用 loopback 来通信敏感信息。20.【推荐】对于不需要使用 File 协议的应用,禁用 File 协议,显式设置 webView.getSettings().setAllowFileAccess(false),对于需要使用 File 协议的应用,禁止 File协议调用 JavaScript,显式设置 webView.getSettings().setJavaScriptEnabled(false)。【强制】Android APP 在 HTTPS 通信中,验证策略需要改成严格模式。 说明:Android APP 在 HTTPS 通信中,使用 ALLOW_ALL_HOSTNAME_VERIFIER,表示允许和所有的 HOST 建立 SSL 通信,这会存在中间人攻击的风险,最终导致敏感信息可能会被劫持,以及其他形式的攻击。22.【推荐】Android5.0 以后安全性要求较高的应用应该使用 window.setFlag(LayoutParam.FLAG_SECURE) 禁止录屏。23.【推荐】zip 中不建议允许../../file 这样的路径,可能被篡改目录结构,造成攻击。 说明:当 zip 压缩包中允许存在"../"的字符串,攻击者可以利用多个"../"在解压时改变zip 文件存放的位置,当文件已经存在是就会进行覆盖,如果覆盖掉的文件是 so、dex 或者 odex 文件,就有可能造成严重的安全问题。24.【强制】开放的 activity/service/receiver 等需要对传入的 intent 做合法性校验。25.【推荐】加密算法:使用不安全的 Hash 算法(MD5/SHA-1)加密信息,存在被破解的风险,建议使用 SHA-256 等安全性更高的 Hash 算法。26.【推荐】Android WebView 组件加载网页发生证书认证错误时,采用默认的处理方法handler.cancel(),停止加载问题页面。说明:Android WebView 组件加载网页发生证书认证错误时,会调用 WebViewClient 类的onReceivedSslError 方法,如果该方法实现调用了 handler.proceed()来忽略该证书错误,则会受到中间人攻击的威胁,可能导致隐私泄露.【推荐】直接传递命令字或者间接处理有敏感信息或操作时,避免使用 socket 实现,使用能够控制权限校验身份的方式通讯。九、其他【强制】不要通过 Msg 传递大的对象,会导致内存问题。【强制】不能使用 System.out.println 打印 log。正例:Log.d(TAG, "Some Android Debug info ...");反例:System.out.println("System out println ...");【强制】Log 的 tag 不能是" "。说明:日志的 tag 是空字符串没有任何意义,也不利于过滤日志。正例:private static String TAG = "LoginActivity"; Log.e(TAG, "Login failed!");反例: Log.e("", "Login failed!");
文章
存储  ·  SQL  ·  编解码  ·  缓存  ·  安全  ·  Java  ·  数据库  ·  Android开发  ·  数据安全/隐私保护  ·  开发者
2022-05-22
Android GridView LruCache
照片墙这种功能现在应该算是挺常见了,在很多应用中你都可以经常看到照片墙的身影。它的设计思路其实也非常简单,用一个GridView控件当作“墙”,然后随着GridView的滚动将一张张照片贴在“墙”上,这些照片可以是手机本地中存储的,也可以是从网上下载的。制作类似于这种的功能的应用,有一个非常重要的问题需要考虑,就是图片资源何时应该释放。因为随着GridView的滚动,加载的图片可能会越来越多,如果没有一种合理的机制对图片进行释放,那么当图片达到一定上限时,程序就必然会崩溃。   今天我们照片墙应用的实现,重点也是放在了如何防止由于图片过多导致程序崩溃上面。主要的核心算法使用了Android中提供的LruCache类,这个类是3.1版本中提供的,如果你是在更早的Android版本中开发,则需要导入android-support-v4的jar包。   关于LruCache用法的详细讲解,可以参考Android高效加载大图、多图方案,有效避免程序OOM。   那我们开始动手吧,新建一个Android项目,起名叫PhotoWallDemo,这里我使用的是Android 4.0的API。   第一个要考虑的问题就是,我们从哪儿去收集这么多的图片呢?这里我从谷歌官方提供的Demo里将图片源取了出来,我们就从这些网址中下载图片,代码如下所示: [java] view plain copy   public class Images {          public final static String[] imageThumbUrls = new String[] {               "https://lh6.googleusercontent.com/-55osAWw3x0Q/URquUtcFr5I/AAAAAAAAAbs/rWlj1RUKrYI/s160-c/A%252520Photographer.jpg",               "https://lh4.googleusercontent.com/--dq8niRp7W4/URquVgmXvgI/AAAAAAAAAbs/-gnuLQfNnBA/s160-c/A%252520Song%252520of%252520Ice%252520and%252520Fire.jpg",               "https://lh5.googleusercontent.com/-7qZeDtRKFKc/URquWZT1gOI/AAAAAAAAAbs/hqWgteyNXsg/s160-c/Another%252520Rockaway%252520Sunset.jpg",               "https://lh3.googleusercontent.com/--L0Km39l5J8/URquXHGcdNI/AAAAAAAAAbs/3ZrSJNrSomQ/s160-c/Antelope%252520Butte.jpg",               "https://lh6.googleusercontent.com/-8HO-4vIFnlw/URquZnsFgtI/AAAAAAAAAbs/WT8jViTF7vw/s160-c/Antelope%252520Hallway.jpg",               "https://lh4.googleusercontent.com/-WIuWgVcU3Qw/URqubRVcj4I/AAAAAAAAAbs/YvbwgGjwdIQ/s160-c/Antelope%252520Walls.jpg",               "https://lh6.googleusercontent.com/-UBmLbPELvoQ/URqucCdv0kI/AAAAAAAAAbs/IdNhr2VQoQs/s160-c/Apre%2525CC%252580s%252520la%252520Pluie.jpg",               "https://lh3.googleusercontent.com/-s-AFpvgSeew/URquc6dF-JI/AAAAAAAAAbs/Mt3xNGRUd68/s160-c/Backlit%252520Cloud.jpg",               "https://lh5.googleusercontent.com/-bvmif9a9YOQ/URquea3heHI/AAAAAAAAAbs/rcr6wyeQtAo/s160-c/Bee%252520and%252520Flower.jpg",               "https://lh5.googleusercontent.com/-n7mdm7I7FGs/URqueT_BT-I/AAAAAAAAAbs/9MYmXlmpSAo/s160-c/Bonzai%252520Rock%252520Sunset.jpg",               "https://lh6.googleusercontent.com/-4CN4X4t0M1k/URqufPozWzI/AAAAAAAAAbs/8wK41lg1KPs/s160-c/Caterpillar.jpg",               "https://lh3.googleusercontent.com/-rrFnVC8xQEg/URqufdrLBaI/AAAAAAAAAbs/s69WYy_fl1E/s160-c/Chess.jpg",               "https://lh5.googleusercontent.com/-WVpRptWH8Yw/URqugh-QmDI/AAAAAAAAAbs/E-MgBgtlUWU/s160-c/Chihuly.jpg",               "https://lh5.googleusercontent.com/-0BDXkYmckbo/URquhKFW84I/AAAAAAAAAbs/ogQtHCTk2JQ/s160-c/Closed%252520Door.jpg",               "https://lh3.googleusercontent.com/-PyggXXZRykM/URquh-kVvoI/AAAAAAAAAbs/hFtDwhtrHHQ/s160-c/Colorado%252520River%252520Sunset.jpg",               "https://lh3.googleusercontent.com/-ZAs4dNZtALc/URquikvOCWI/AAAAAAAAAbs/DXz4h3dll1Y/s160-c/Colors%252520of%252520Autumn.jpg",               "https://lh4.googleusercontent.com/-GztnWEIiMz8/URqukVCU7bI/AAAAAAAAAbs/jo2Hjv6MZ6M/s160-c/Countryside.jpg",               "https://lh4.googleusercontent.com/-bEg9EZ9QoiM/URquklz3FGI/AAAAAAAAAbs/UUuv8Ac2BaE/s160-c/Death%252520Valley%252520-%252520Dunes.jpg",               "https://lh6.googleusercontent.com/-ijQJ8W68tEE/URqulGkvFEI/AAAAAAAAAbs/zPXvIwi_rFw/s160-c/Delicate%252520Arch.jpg",               "https://lh5.googleusercontent.com/-Oh8mMy2ieng/URqullDwehI/AAAAAAAAAbs/TbdeEfsaIZY/s160-c/Despair.jpg",               "https://lh5.googleusercontent.com/-gl0y4UiAOlk/URqumC_KjBI/AAAAAAAAAbs/PM1eT7dn4oo/s160-c/Eagle%252520Fall%252520Sunrise.jpg",               "https://lh3.googleusercontent.com/-hYYHd2_vXPQ/URqumtJa9eI/AAAAAAAAAbs/wAalXVkbSh0/s160-c/Electric%252520Storm.jpg",               "https://lh5.googleusercontent.com/-PyY_yiyjPTo/URqunUOhHFI/AAAAAAAAAbs/azZoULNuJXc/s160-c/False%252520Kiva.jpg",               "https://lh6.googleusercontent.com/-PYvLVdvXywk/URqunwd8hfI/AAAAAAAAAbs/qiMwgkFvf6I/s160-c/Fitzgerald%252520Streaks.jpg",               "https://lh4.googleusercontent.com/-KIR_UobIIqY/URquoCZ9SlI/AAAAAAAAAbs/Y4d4q8sXu4c/s160-c/Foggy%252520Sunset.jpg",               "https://lh6.googleusercontent.com/-9lzOk_OWZH0/URquoo4xYoI/AAAAAAAAAbs/AwgzHtNVCwU/s160-c/Frantic.jpg",               "https://lh3.googleusercontent.com/-0X3JNaKaz48/URqupH78wpI/AAAAAAAAAbs/lHXxu_zbH8s/s160-c/Golden%252520Gate%252520Afternoon.jpg",               "https://lh6.googleusercontent.com/-95sb5ag7ABc/URqupl95RDI/AAAAAAAAAbs/g73R20iVTRA/s160-c/Golden%252520Gate%252520Fog.jpg",               "https://lh3.googleusercontent.com/-JB9v6rtgHhk/URqup21F-zI/AAAAAAAAAbs/64Fb8qMZWXk/s160-c/Golden%252520Grass.jpg",               "https://lh4.googleusercontent.com/-EIBGfnuLtII/URquqVHwaRI/AAAAAAAAAbs/FA4McV2u8VE/s160-c/Grand%252520Teton.jpg",               "https://lh4.googleusercontent.com/-WoMxZvmN9nY/URquq1v2AoI/AAAAAAAAAbs/grj5uMhL6NA/s160-c/Grass%252520Closeup.jpg",               "https://lh3.googleusercontent.com/-6hZiEHXx64Q/URqurxvNdqI/AAAAAAAAAbs/kWMXM3o5OVI/s160-c/Green%252520Grass.jpg",               "https://lh5.googleusercontent.com/-6LVb9OXtQ60/URquteBFuKI/AAAAAAAAAbs/4F4kRgecwFs/s160-c/Hanging%252520Leaf.jpg",               "https://lh4.googleusercontent.com/-zAvf__52ONk/URqutT_IuxI/AAAAAAAAAbs/D_bcuc0thoU/s160-c/Highway%2525201.jpg",               "https://lh6.googleusercontent.com/-H4SrUg615rA/URquuL27fXI/AAAAAAAAAbs/4aEqJfiMsOU/s160-c/Horseshoe%252520Bend%252520Sunset.jpg",               "https://lh4.googleusercontent.com/-JhFi4fb_Pqw/URquuX-QXbI/AAAAAAAAAbs/IXpYUxuweYM/s160-c/Horseshoe%252520Bend.jpg",               "https://lh5.googleusercontent.com/-UGgssvFRJ7g/URquueyJzGI/AAAAAAAAAbs/yYIBlLT0toM/s160-c/Into%252520the%252520Blue.jpg",               "https://lh3.googleusercontent.com/-CH7KoupI7uI/URquu0FF__I/AAAAAAAAAbs/R7GDmI7v_G0/s160-c/Jelly%252520Fish%2525202.jpg",               "https://lh4.googleusercontent.com/-pwuuw6yhg8U/URquvPxR3FI/AAAAAAAAAbs/VNGk6f-tsGE/s160-c/Jelly%252520Fish%2525203.jpg",               "https://lh5.googleusercontent.com/-GoUQVw1fnFw/URquv6xbC0I/AAAAAAAAAbs/zEUVTQQ43Zc/s160-c/Kauai.jpg",               "https://lh6.googleusercontent.com/-8QdYYQEpYjw/URquwvdh88I/AAAAAAAAAbs/cktDy-ysfHo/s160-c/Kyoto%252520Sunset.jpg",               "https://lh4.googleusercontent.com/-vPeekyDjOE0/URquwzJ28qI/AAAAAAAAAbs/qxcyXULsZrg/s160-c/Lake%252520Tahoe%252520Colors.jpg",               "https://lh4.googleusercontent.com/-xBPxWpD4yxU/URquxWHk8AI/AAAAAAAAAbs/ARDPeDYPiMY/s160-c/Lava%252520from%252520the%252520Sky.jpg",               "https://lh3.googleusercontent.com/-897VXrJB6RE/URquxxxd-5I/AAAAAAAAAbs/j-Cz4T4YvIw/s160-c/Leica%25252050mm%252520Summilux.jpg",               "https://lh5.googleusercontent.com/-qSJ4D4iXzGo/URquyDWiJ1I/AAAAAAAAAbs/k2pBXeWehOA/s160-c/Leica%25252050mm%252520Summilux.jpg",               "https://lh6.googleusercontent.com/-dwlPg83vzLg/URquylTVuFI/AAAAAAAAAbs/G6SyQ8b4YsI/s160-c/Leica%252520M8%252520%252528Front%252529.jpg",               "https://lh3.googleusercontent.com/-R3_EYAyJvfk/URquzQBv8eI/AAAAAAAAAbs/b9xhpUM3pEI/s160-c/Light%252520to%252520Sand.jpg",               "https://lh3.googleusercontent.com/-fHY5h67QPi0/URqu0Cp4J1I/AAAAAAAAAbs/0lG6m94Z6vM/s160-c/Little%252520Bit%252520of%252520Paradise.jpg",               "https://lh5.googleusercontent.com/-TzF_LwrCnRM/URqu0RddPOI/AAAAAAAAAbs/gaj2dLiuX0s/s160-c/Lone%252520Pine%252520Sunset.jpg",               "https://lh3.googleusercontent.com/-4HdpJ4_DXU4/URqu046dJ9I/AAAAAAAAAbs/eBOodtk2_uk/s160-c/Lonely%252520Rock.jpg",               "https://lh6.googleusercontent.com/-erbF--z-W4s/URqu1ajSLkI/AAAAAAAAAbs/xjDCDO1INzM/s160-c/Longue%252520Vue.jpg",               "https://lh6.googleusercontent.com/-0CXJRdJaqvc/URqu1opNZNI/AAAAAAAAAbs/PFB2oPUU7Lk/s160-c/Look%252520Me%252520in%252520the%252520Eye.jpg",               "https://lh3.googleusercontent.com/-D_5lNxnDN6g/URqu2Tk7HVI/AAAAAAAAAbs/p0ddca9W__Y/s160-c/Lost%252520in%252520a%252520Field.jpg",               "https://lh6.googleusercontent.com/-flsqwMrIk2Q/URqu24PcmjI/AAAAAAAAAbs/5ocIH85XofM/s160-c/Marshall%252520Beach%252520Sunset.jpg",               "https://lh4.googleusercontent.com/-Y4lgryEVTmU/URqu28kG3gI/AAAAAAAAAbs/OjXpekqtbJ4/s160-c/Mono%252520Lake%252520Blue.jpg",               "https://lh4.googleusercontent.com/-AaHAJPmcGYA/URqu3PIldHI/AAAAAAAAAbs/lcTqk1SIcRs/s160-c/Monument%252520Valley%252520Overlook.jpg",               "https://lh4.googleusercontent.com/-vKxfdQ83dQA/URqu31Yq_BI/AAAAAAAAAbs/OUoGk_2AyfM/s160-c/Moving%252520Rock.jpg",               "https://lh5.googleusercontent.com/-CG62QiPpWXg/URqu4ia4vRI/AAAAAAAAAbs/0YOdqLAlcAc/s160-c/Napali%252520Coast.jpg",               "https://lh6.googleusercontent.com/-wdGrP5PMmJQ/URqu5PZvn7I/AAAAAAAAAbs/m0abEcdPXe4/s160-c/One%252520Wheel.jpg",               "https://lh6.googleusercontent.com/-6WS5DoCGuOA/URqu5qx1UgI/AAAAAAAAAbs/giMw2ixPvrY/s160-c/Open%252520Sky.jpg",               "https://lh6.googleusercontent.com/-u8EHKj8G8GQ/URqu55sM6yI/AAAAAAAAAbs/lIXX_GlTdmI/s160-c/Orange%252520Sunset.jpg",               "https://lh6.googleusercontent.com/-74Z5qj4bTDE/URqu6LSrJrI/AAAAAAAAAbs/XzmVkw90szQ/s160-c/Orchid.jpg",               "https://lh6.googleusercontent.com/-lEQE4h6TePE/URqu6t_lSkI/AAAAAAAAAbs/zvGYKOea_qY/s160-c/Over%252520there.jpg",               "https://lh5.googleusercontent.com/-cauH-53JH2M/URqu66v_USI/AAAAAAAAAbs/EucwwqclfKQ/s160-c/Plumes.jpg",               "https://lh3.googleusercontent.com/-eDLT2jHDoy4/URqu7axzkAI/AAAAAAAAAbs/iVZE-xJ7lZs/s160-c/Rainbokeh.jpg",               "https://lh5.googleusercontent.com/-j1NLqEFIyco/URqu8L1CGcI/AAAAAAAAAbs/aqZkgX66zlI/s160-c/Rainbow.jpg",               "https://lh5.googleusercontent.com/-DRnqmK0t4VU/URqu8XYN9yI/AAAAAAAAAbs/LgvF_592WLU/s160-c/Rice%252520Fields.jpg",               "https://lh3.googleusercontent.com/-hwh1v3EOGcQ/URqu8qOaKwI/AAAAAAAAAbs/IljRJRnbJGw/s160-c/Rockaway%252520Fire%252520Sky.jpg",               "https://lh5.googleusercontent.com/-wjV6FQk7tlk/URqu9jCQ8sI/AAAAAAAAAbs/RyYUpdo-c9o/s160-c/Rockaway%252520Flow.jpg",               "https://lh6.googleusercontent.com/-6cAXNfo7D20/URqu-BdzgPI/AAAAAAAAAbs/OmsYllzJqwo/s160-c/Rockaway%252520Sunset%252520Sky.jpg",               "https://lh3.googleusercontent.com/-sl8fpGPS-RE/URqu_BOkfgI/AAAAAAAAAbs/Dg2Fv-JxOeg/s160-c/Russian%252520Ridge%252520Sunset.jpg",               "https://lh6.googleusercontent.com/-gVtY36mMBIg/URqu_q91lkI/AAAAAAAAAbs/3CiFMBcy5MA/s160-c/Rust%252520Knot.jpg",               "https://lh6.googleusercontent.com/-GHeImuHqJBE/URqu_FKfVLI/AAAAAAAAAbs/axuEJeqam7Q/s160-c/Sailing%252520Stones.jpg",               "https://lh3.googleusercontent.com/-hBbYZjTOwGc/URqu_ycpIrI/AAAAAAAAAbs/nAdJUXnGJYE/s160-c/Seahorse.jpg",               "https://lh3.googleusercontent.com/-Iwi6-i6IexY/URqvAYZHsVI/AAAAAAAAAbs/5ETWl4qXsFE/s160-c/Shinjuku%252520Street.jpg",               "https://lh6.googleusercontent.com/-amhnySTM_MY/URqvAlb5KoI/AAAAAAAAAbs/pFCFgzlKsn0/s160-c/Sierra%252520Heavens.jpg",               "https://lh5.googleusercontent.com/-dJgjepFrYSo/URqvBVJZrAI/AAAAAAAAAbs/v-F5QWpYO6s/s160-c/Sierra%252520Sunset.jpg",               "https://lh4.googleusercontent.com/-Z4zGiC5nWdc/URqvBdEwivI/AAAAAAAAAbs/ZRZR1VJ84QA/s160-c/Sin%252520Lights.jpg",               "https://lh4.googleusercontent.com/-_0cYiWW8ccY/URqvBz3iM4I/AAAAAAAAAbs/9N_Wq8MhLTY/s160-c/Starry%252520Lake.jpg",               "https://lh3.googleusercontent.com/-A9LMoRyuQUA/URqvCYx_JoI/AAAAAAAAAbs/s7sde1Bz9cI/s160-c/Starry%252520Night.jpg",               "https://lh3.googleusercontent.com/-KtLJ3k858eY/URqvC_2h_bI/AAAAAAAAAbs/zzEBImwDA_g/s160-c/Stream.jpg",               "https://lh5.googleusercontent.com/-dFB7Lad6RcA/URqvDUftwWI/AAAAAAAAAbs/BrhoUtXTN7o/s160-c/Strip%252520Sunset.jpg",               "https://lh5.googleusercontent.com/-at6apgFiN20/URqvDyffUZI/AAAAAAAAAbs/clABCx171bE/s160-c/Sunset%252520Hills.jpg",               "https://lh4.googleusercontent.com/-7-EHhtQthII/URqvEYTk4vI/AAAAAAAAAbs/QSJZoB3YjVg/s160-c/Tenaya%252520Lake%2525202.jpg",               "https://lh6.googleusercontent.com/-8MrjV_a-Pok/URqvFC5repI/AAAAAAAAAbs/9inKTg9fbCE/s160-c/Tenaya%252520Lake.jpg",               "https://lh5.googleusercontent.com/-B1HW-z4zwao/URqvFWYRwUI/AAAAAAAAAbs/8Peli53Bs8I/s160-c/The%252520Cave%252520BW.jpg",               "https://lh3.googleusercontent.com/-PO4E-xZKAnQ/URqvGRqjYkI/AAAAAAAAAbs/42nyADFsXag/s160-c/The%252520Fisherman.jpg",               "https://lh4.googleusercontent.com/-iLyZlzfdy7s/URqvG0YScdI/AAAAAAAAAbs/1J9eDKmkXtk/s160-c/The%252520Night%252520is%252520Coming.jpg",               "https://lh6.googleusercontent.com/-G-k7YkkUco0/URqvHhah6fI/AAAAAAAAAbs/_taQQG7t0vo/s160-c/The%252520Road.jpg",               "https://lh6.googleusercontent.com/-h-ALJt7kSus/URqvIThqYfI/AAAAAAAAAbs/ejiv35olWS8/s160-c/Tokyo%252520Heights.jpg",               "https://lh5.googleusercontent.com/-Hy9k-TbS7xg/URqvIjQMOxI/AAAAAAAAAbs/RSpmmOATSkg/s160-c/Tokyo%252520Highway.jpg",               "https://lh6.googleusercontent.com/-83oOvMb4OZs/URqvJL0T7lI/AAAAAAAAAbs/c5TECZ6RONM/s160-c/Tokyo%252520Smog.jpg",               "https://lh3.googleusercontent.com/-FB-jfgREEfI/URqvJI3EXAI/AAAAAAAAAbs/XfyweiRF4v8/s160-c/Tufa%252520at%252520Night.jpg",               "https://lh4.googleusercontent.com/-vngKD5Z1U8w/URqvJUCEgPI/AAAAAAAAAbs/ulxCMVcU6EU/s160-c/Valley%252520Sunset.jpg",               "https://lh6.googleusercontent.com/-DOz5I2E2oMQ/URqvKMND1kI/AAAAAAAAAbs/Iqf0IsInleo/s160-c/Windmill%252520Sunrise.jpg",               "https://lh5.googleusercontent.com/-biyiyWcJ9MU/URqvKculiAI/AAAAAAAAAbs/jyPsCplJOpE/s160-c/Windmill.jpg",               "https://lh4.googleusercontent.com/-PDT167_xRdA/URqvK36mLcI/AAAAAAAAAbs/oi2ik9QseMI/s160-c/Windmills.jpg",               "https://lh5.googleusercontent.com/-kI_QdYx7VlU/URqvLXCB6gI/AAAAAAAAAbs/N31vlZ6u89o/s160-c/Yet%252520Another%252520Rockaway%252520Sunset.jpg",               "https://lh4.googleusercontent.com/-e9NHZ5k5MSs/URqvMIBZjtI/AAAAAAAAAbs/1fV810rDNfQ/s160-c/Yosemite%252520Tree.jpg", };   }   图片源已经有了,现在我们就该考虑在哪里放置这些图片了。新建或打开activity_main.xml作为程序的主布局,加入如下代码: [html] view plain copy   <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"       xmlns:tools="http://schemas.android.com/tools"       android:layout_width="wrap_content"       android:layout_height="wrap_content" >              <GridView            android:id="@+id/photo_wall"           android:layout_width="match_parent"           android:layout_height="wrap_content"           android:columnWidth="90dip"           android:stretchMode="columnWidth"           android:numColumns="auto_fit"           android:verticalSpacing="10dip"           android:gravity="center"           ></GridView>          </LinearLayout>   可以看到,我们在这个布局文件中仅加入了一个GridView,这也就是我们程序中的“墙”,所有的图片都将贴在这个“墙”上。   接着我们定义GridView中每一个子View的布局,新建一个photo_layout.xml布局,加入如下代码: [html] view plain copy   <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"       xmlns:tools="http://schemas.android.com/tools"       android:layout_width="wrap_content"       android:layout_height="wrap_content" >          <ImageView            android:id="@+id/photo"           android:layout_width="90dip"           android:layout_height="90dip"           android:src="@drawable/empty_photo"           android:layout_centerInParent="true"           />      </RelativeLayout>   在每一个子View中我们就简单使用了一个ImageView来显示一张图片。这样所有的布局就已经定义好了。   接下来新建PhotoWallAdapter做为GridView的适配器,代码如下所示: [java] view plain copy   public class PhotoWallAdapter extends ArrayAdapter<String> implements OnScrollListener {          /**       * 记录所有正在下载或等待下载的任务。       */       private Set<BitmapWorkerTask> taskCollection;          /**       * 图片缓存技术的核心类,用于缓存所有下载好的图片,在程序内存达到设定值时会将最少最近使用的图片移除掉。       */       private LruCache<String, Bitmap> mMemoryCache;          /**       * GridView的实例       */       private GridView mPhotoWall;          /**       * 第一张可见图片的下标       */       private int mFirstVisibleItem;          /**       * 一屏有多少张图片可见       */       private int mVisibleItemCount;          /**       * 记录是否刚打开程序,用于解决进入程序不滚动屏幕,不会下载图片的问题。       */       private boolean isFirstEnter = true;          public PhotoWallAdapter(Context context, int textViewResourceId, String[] objects,               GridView photoWall) {           super(context, textViewResourceId, objects);           mPhotoWall = photoWall;           taskCollection = new HashSet<BitmapWorkerTask>();           // 获取应用程序最大可用内存           int maxMemory = (int) Runtime.getRuntime().maxMemory();           int cacheSize = maxMemory / 8;           // 设置图片缓存大小为程序最大可用内存的1/8           mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {               @Override               protected int sizeOf(String key, Bitmap bitmap) {                   return bitmap.getByteCount();               }           };           mPhotoWall.setOnScrollListener(this);       }          @Override       public View getView(int position, View convertView, ViewGroup parent) {           final String url = getItem(position);           View view;           if (convertView == null) {               view = LayoutInflater.from(getContext()).inflate(R.layout.photo_layout, null);           } else {               view = convertView;           }           final ImageView photo = (ImageView) view.findViewById(R.id.photo);           // 给ImageView设置一个Tag,保证异步加载图片时不会乱序           photo.setTag(url);           setImageView(url, photo);           return view;       }          /**       * 给ImageView设置图片。首先从LruCache中取出图片的缓存,设置到ImageView上。如果LruCache中没有该图片的缓存,       * 就给ImageView设置一张默认图片。       *        * @param imageUrl       *            图片的URL地址,用于作为LruCache的键。       * @param imageView       *            用于显示图片的控件。       */       private void setImageView(String imageUrl, ImageView imageView) {           Bitmap bitmap = getBitmapFromMemoryCache(imageUrl);           if (bitmap != null) {               imageView.setImageBitmap(bitmap);           } else {               imageView.setImageResource(R.drawable.empty_photo);           }       }          /**       * 将一张图片存储到LruCache中。       *        * @param key       *            LruCache的键,这里传入图片的URL地址。       * @param bitmap       *            LruCache的键,这里传入从网络上下载的Bitmap对象。       */       public void addBitmapToMemoryCache(String key, Bitmap bitmap) {           if (getBitmapFromMemoryCache(key) == null) {               mMemoryCache.put(key, bitmap);           }       }          /**       * 从LruCache中获取一张图片,如果不存在就返回null。       *        * @param key       *            LruCache的键,这里传入图片的URL地址。       * @return 对应传入键的Bitmap对象,或者null。       */       public Bitmap getBitmapFromMemoryCache(String key) {           return mMemoryCache.get(key);       }          @Override       public void onScrollStateChanged(AbsListView view, int scrollState) {           // 仅当GridView静止时才去下载图片,GridView滑动时取消所有正在下载的任务           if (scrollState == SCROLL_STATE_IDLE) {               loadBitmaps(mFirstVisibleItem, mVisibleItemCount);           } else {               cancelAllTasks();           }       }          @Override       public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,               int totalItemCount) {           mFirstVisibleItem = firstVisibleItem;           mVisibleItemCount = visibleItemCount;           // 下载的任务应该由onScrollStateChanged里调用,但首次进入程序时onScrollStateChanged并不会调用,           // 因此在这里为首次进入程序开启下载任务。           if (isFirstEnter && visibleItemCount > 0) {               loadBitmaps(firstVisibleItem, visibleItemCount);               isFirstEnter = false;           }       }          /**       * 加载Bitmap对象。此方法会在LruCache中检查所有屏幕中可见的ImageView的Bitmap对象,       * 如果发现任何一个ImageView的Bitmap对象不在缓存中,就会开启异步线程去下载图片。       *        * @param firstVisibleItem       *            第一个可见的ImageView的下标       * @param visibleItemCount       *            屏幕中总共可见的元素数       */       private void loadBitmaps(int firstVisibleItem, int visibleItemCount) {           try {               for (int i = firstVisibleItem; i < firstVisibleItem + visibleItemCount; i++) {                   String imageUrl = Images.imageThumbUrls[i];                   Bitmap bitmap = getBitmapFromMemoryCache(imageUrl);                   if (bitmap == null) {                       BitmapWorkerTask task = new BitmapWorkerTask();                       taskCollection.add(task);                       task.execute(imageUrl);                   } else {                       ImageView imageView = (ImageView) mPhotoWall.findViewWithTag(imageUrl);                       if (imageView != null && bitmap != null) {                           imageView.setImageBitmap(bitmap);                       }                   }               }           } catch (Exception e) {               e.printStackTrace();           }       }          /**       * 取消所有正在下载或等待下载的任务。       */       public void cancelAllTasks() {           if (taskCollection != null) {               for (BitmapWorkerTask task : taskCollection) {                   task.cancel(false);               }           }       }          /**       * 异步下载图片的任务。       *        * @author guolin       */       class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> {              /**           * 图片的URL地址           */           private String imageUrl;              @Override           protected Bitmap doInBackground(String... params) {               imageUrl = params[0];               // 在后台开始下载图片               Bitmap bitmap = downloadBitmap(params[0]);               if (bitmap != null) {                   // 图片下载完成后缓存到LrcCache中                   addBitmapToMemoryCache(params[0], bitmap);               }               return bitmap;           }              @Override           protected void onPostExecute(Bitmap bitmap) {               super.onPostExecute(bitmap);               // 根据Tag找到相应的ImageView控件,将下载好的图片显示出来。               ImageView imageView = (ImageView) mPhotoWall.findViewWithTag(imageUrl);               if (imageView != null && bitmap != null) {                   imageView.setImageBitmap(bitmap);               }               taskCollection.remove(this);           }              /**           * 建立HTTP请求,并获取Bitmap对象。           *            * @param imageUrl           *            图片的URL地址           * @return 解析后的Bitmap对象           */           private Bitmap downloadBitmap(String imageUrl) {               Bitmap bitmap = null;               HttpURLConnection con = null;               try {                   URL url = new URL(imageUrl);                   con = (HttpURLConnection) url.openConnection();                   con.setConnectTimeout(5 * 1000);                   con.setReadTimeout(10 * 1000);                   bitmap = BitmapFactory.decodeStream(con.getInputStream());               } catch (Exception e) {                   e.printStackTrace();               } finally {                   if (con != null) {                       con.disconnect();                   }               }               return bitmap;           }          }      }   PhotoWallAdapter是整个照片墙程序中最关键的一个类了,这里我来重点给大家讲解一下。首先在PhotoWallAdapter的构造函数中,我们初始化了LruCache类,并设置了最大缓存容量为程序最大可用内存的1/8,接下来又为GridView注册了一个滚动监听器。然后在getView()方法中,我们为每个ImageView设置了一个唯一的Tag,这个Tag的作用是为了后面能够准确地找回这个ImageView,不然异步加载图片会出现乱序的情况。之后调用了setImageView()方法为ImageView设置一张图片,这个方法首先会从LruCache缓存中查找是否已经缓存了这张图片,如果成功找到则将缓存中的图片显示在ImageView上,否则就显示一张默认的空图片。   看了半天,那到底是在哪里下载图片的呢?这是在GridView的滚动监听器中进行的,在onScrollStateChanged()方法中,我们对GridView的滚动状态进行了判断,如果当前GridView是静止的,则调用loadBitmaps()方法去下载图片,如果GridView正在滚动,则取消掉所有下载任务,这样可以保证GridView滚动的流畅性。在loadBitmaps()方法中,我们为屏幕上所有可见的GridView子元素开启了一个线程去执行下载任务,下载成功后将图片存储到LruCache当中,然后通过Tag找到相应的ImageView控件,把下载好的图片显示出来。   由于我们使用了LruCache来缓存图片,所以不需要担心内存溢出的情况,当LruCache中存储图片的总大小达到容量上限的时候,会自动把最近最少使用的图片从缓存中移除。   最后新建或打开MainActivity作为程序的主Activity,代码如下所示: [java] view plain copy   public class MainActivity extends Activity {          /**       * 用于展示照片墙的GridView       */       private GridView mPhotoWall;          /**       * GridView的适配器       */       private PhotoWallAdapter adapter;          @Override       protected void onCreate(Bundle savedInstanceState) {           super.onCreate(savedInstanceState);           setContentView(R.layout.activity_main);           mPhotoWall = (GridView) findViewById(R.id.photo_wall);           adapter = new PhotoWallAdapter(this, 0, Images.imageThumbUrls, mPhotoWall);           mPhotoWall.setAdapter(adapter);       }          @Override       protected void onDestroy() {           super.onDestroy();           // 退出程序时结束所有的下载任务           adapter.cancelAllTasks();       }      }   MainActivity中的代码非常简单,没什么需要说明的了,在Activity被销毁时取消掉了所有的下载任务,避免程序在后台耗费流量。另外由于我们使用了网络功能,别忘了在AndroidManifest.xml中加入网络权限的声明。   现在可以运行一下程序了,效果如下图所示:   可以看到,滚动照片墙,会异步加载图片到相应的ImageView上。随着加载图片的增多,会释放掉一些之前加载过的图片,你多滚动几次就可以看得出了。另外为了能让大家明显看出图片的释放情况,我在这个程序中没有使用本地缓存,所有被释放掉的图片再次显示需要从网络上再下载一遍。在实际的项目中配合适当的本地缓存效果会更好。   打开DDMS,我们可以发现,由于有LruCache帮我们管理图片缓存,不管如何滚动照片墙,程序内存始终会保持在一个合理的范围内。     本篇文章的重点在于如何对图片进行更好的回收,因此照片墙只是简单地使用GridView进行了展示,想要看更酷更炫的照片墙效果的朋友,可以参考我后面的一篇文章 Android瀑布流照片墙实现,体验不规则排列的美感 。   好了,今天的讲解到此结束,有疑问的朋友请在下面留言。 本文转自农夫山泉别墅博客园博客,原文链接:http://www.cnblogs.com/yaowen/p/6347857.html,如需转载请自行联系原作者
文章
存储  ·  缓存  ·  Java  ·  Android开发
2017-11-14
利用LruCache为GridView异步加载大量网络图片完整示例
MainActivity如下: package cc.testlrucache; import android.os.Bundle; import android.widget.GridView; import android.app.Activity; /** * Demo描述: * 在GridView中采用LruCache异步加载大量图片,避免OOM * * 学习资料: * http://blog.csdn.net/guolin_blog/article/details/9526203 * Thank you very much */ public class MainActivity extends Activity { private GridView mGridView; private GridViewAdapter mGridViewAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); init(); } private void init(){ mGridView = (GridView) findViewById(R.id.gridView); mGridViewAdapter = new GridViewAdapter(this, 0, ImagesUrl.Urls, mGridView); mGridView.setAdapter(mGridViewAdapter); } //取消所有的下载任务 @Override protected void onDestroy() { super.onDestroy(); mGridViewAdapter.cancelAllTasks(); } } GridViewAdapter如下: package cc.testlrucache; import java.net.HttpURLConnection; import java.net.URL; import java.util.HashSet; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.AsyncTask; import android.support.v4.util.LruCache; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.AbsListView.OnScrollListener; import android.widget.ArrayAdapter; import android.widget.GridView; import android.widget.ImageView; /** * LruCache的流程分析: * 我们从第一次进入应用的情况下开始 * 1 依据图片的Url从LruCache缓存中取图片. * 若图片存在缓存中,则显示该图片;否则显示默认图片 * 2 因为是第一次进入该界面所以会执行: * loadBitmaps(firstVisibleItem, visibleItemCount); * 我们从loadBitmaps()方法作为切入点,继续往下梳理 * 3 尝试从LruCache缓存中取图片.如果在显示即可,否则进入4 * 4 开启一个异步任务下载图片.下载完成后显示图片,并且将 * 该图片存入LruCache缓存中 * * 在停止滑动时,会调用loadBitmaps(firstVisibleItem, visibleItemCount) * 情况与上类似 */ @SuppressLint("NewApi") public class GridViewAdapter extends ArrayAdapter<String> { private GridView mGridView; //图片缓存类 private LruCache<String, Bitmap> mLruCache; //记录所有正在下载或等待下载的任务 private HashSet<DownloadBitmapAsyncTask> mDownloadBitmapAsyncTaskHashSet; //GridView中可见的第一张图片的下标 private int mFirstVisibleItem; //GridView中可见的图片的数量 private int mVisibleItemCount; //记录是否是第一次进入该界面 private boolean isFirstEnterThisActivity = true; public GridViewAdapter(Context context, int textViewResourceId,String[] objects, GridView gridView) { super(context, textViewResourceId, objects); mGridView = gridView; mGridView.setOnScrollListener(new ScrollListenerImpl()); mDownloadBitmapAsyncTaskHashSet = new HashSet<DownloadBitmapAsyncTask>(); // 获取应用程序最大可用内存 int maxMemory = (int) Runtime.getRuntime().maxMemory(); // 设置图片缓存大小为maxMemory的1/6 int cacheSize = maxMemory/6; mLruCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getByteCount(); } }; } @Override public View getView(int position, View convertView, ViewGroup parent) { String url = getItem(position); View view; if (convertView == null) { view = LayoutInflater.from(getContext()).inflate(R.layout.gridview_item, null); } else { view = convertView; } ImageView imageView = (ImageView) view.findViewById(R.id.imageView); //为该ImageView设置一个Tag,防止图片错位 imageView.setTag(url); //为该ImageView设置显示的图片 setImageForImageView(url, imageView); return view; } /** * 为ImageView设置图片(Image) * 1 从缓存中获取图片 * 2 若图片不在缓存中则为其设置默认图片 */ private void setImageForImageView(String imageUrl, ImageView imageView) { Bitmap bitmap = getBitmapFromLruCache(imageUrl); if (bitmap != null) { imageView.setImageBitmap(bitmap); } else { imageView.setImageResource(R.drawable.default_image); } } /** * 将图片存储到LruCache */ public void addBitmapToLruCache(String key, Bitmap bitmap) { if (getBitmapFromLruCache(key) == null) { mLruCache.put(key, bitmap); } } /** * 从LruCache缓存获取图片 */ public Bitmap getBitmapFromLruCache(String key) { return mLruCache.get(key); } /** * 为GridView的item加载图片 * * @param firstVisibleItem * GridView中可见的第一张图片的下标 * * @param visibleItemCount * GridView中可见的图片的数量 * */ private void loadBitmaps(int firstVisibleItem, int visibleItemCount) { try { for (int i = firstVisibleItem; i < firstVisibleItem + visibleItemCount; i++) { String imageUrl = ImagesUrl.Urls[i]; Bitmap bitmap = getBitmapFromLruCache(imageUrl); if (bitmap == null) { DownloadBitmapAsyncTask downloadBitmapAsyncTask = new DownloadBitmapAsyncTask(); mDownloadBitmapAsyncTaskHashSet.add(downloadBitmapAsyncTask); downloadBitmapAsyncTask.execute(imageUrl); } else { //依据Tag找到对应的ImageView显示图片 ImageView imageView = (ImageView) mGridView.findViewWithTag(imageUrl); if (imageView != null && bitmap != null) { imageView.setImageBitmap(bitmap); } } } } catch (Exception e) { e.printStackTrace(); } } /** * 取消所有正在下载或等待下载的任务 */ public void cancelAllTasks() { if (mDownloadBitmapAsyncTaskHashSet != null) { for (DownloadBitmapAsyncTask task : mDownloadBitmapAsyncTaskHashSet) { task.cancel(false); } } } private class ScrollListenerImpl implements OnScrollListener{ /** * * 我们的本意是通过onScrollStateChanged获知:每次GridView停止滑动时加载图片 * 但是存在一个特殊情况: * 当第一次入应用的时候,此时并没有滑动屏幕的操作即不会调用onScrollStateChanged,但应该加载图片. * 所以在此处做一个特殊的处理. * 即代码: * if (isFirstEnterThisActivity && visibleItemCount > 0) { * loadBitmaps(firstVisibleItem, visibleItemCount); * isFirstEnterThisActivity = false; * } * * ------------------------------------------------------------ * * 其余的都是正常情况. * 所以我们需要不断保存:firstVisibleItem和visibleItemCount * 从而便于中在onScrollStateChanged()判断当停止滑动时加载图片 * */ @Override public void onScroll(AbsListView view, int firstVisibleItem,int visibleItemCount, int totalItemCount) { mFirstVisibleItem = firstVisibleItem; mVisibleItemCount = visibleItemCount; if (isFirstEnterThisActivity && visibleItemCount > 0) { loadBitmaps(firstVisibleItem, visibleItemCount); isFirstEnterThisActivity = false; } } /** * GridView停止滑动时下载图片 * 其余情况下取消所有正在下载或者等待下载的任务 */ @Override public void onScrollStateChanged(AbsListView view, int scrollState) { if (scrollState == SCROLL_STATE_IDLE) { loadBitmaps(mFirstVisibleItem, mVisibleItemCount); } else { cancelAllTasks(); } } } /** * 下载图片的异步任务 */ class DownloadBitmapAsyncTask extends AsyncTask<String, Void, Bitmap> { private String imageUrl; @Override protected Bitmap doInBackground(String... params) { imageUrl = params[0]; Bitmap bitmap = downloadBitmap(params[0]); if (bitmap != null) { //下载完后,将其缓存到LrcCache addBitmapToLruCache(params[0], bitmap); } return bitmap; } @Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); //下载完成后,找到其对应的ImageView显示图片 ImageView imageView = (ImageView) mGridView.findViewWithTag(imageUrl); if (imageView != null && bitmap != null) { imageView.setImageBitmap(bitmap); } mDownloadBitmapAsyncTaskHashSet.remove(this); } } // 获取Bitmap private Bitmap downloadBitmap(String imageUrl) { Bitmap bitmap = null; HttpURLConnection httpURLConnection = null; try { URL url = new URL(imageUrl); httpURLConnection = (HttpURLConnection) url.openConnection(); httpURLConnection.setConnectTimeout(5 * 1000); httpURLConnection.setReadTimeout(10 * 1000); httpURLConnection.setDoInput(true); httpURLConnection.setDoOutput(true); bitmap = BitmapFactory.decodeStream(httpURLConnection.getInputStream()); } catch (Exception e) { e.printStackTrace(); } finally { if (httpURLConnection != null) { httpURLConnection.disconnect(); } } return bitmap; } } ImagesUrl如下: package cc.testlrucache; public class ImagesUrl { public final static String[] Urls = new String[] { "https://lh6.googleusercontent.com/-55osAWw3x0Q/URquUtcFr5I/AAAAAAAAAbs/rWlj1RUKrYI/s160-c/A%252520Photographer.jpg", "https://lh4.googleusercontent.com/--dq8niRp7W4/URquVgmXvgI/AAAAAAAAAbs/-gnuLQfNnBA/s160-c/A%252520Song%252520of%252520Ice%252520and%252520Fire.jpg", "https://lh5.googleusercontent.com/-7qZeDtRKFKc/URquWZT1gOI/AAAAAAAAAbs/hqWgteyNXsg/s160-c/Another%252520Rockaway%252520Sunset.jpg", "https://lh3.googleusercontent.com/--L0Km39l5J8/URquXHGcdNI/AAAAAAAAAbs/3ZrSJNrSomQ/s160-c/Antelope%252520Butte.jpg", "https://lh6.googleusercontent.com/-8HO-4vIFnlw/URquZnsFgtI/AAAAAAAAAbs/WT8jViTF7vw/s160-c/Antelope%252520Hallway.jpg", "https://lh4.googleusercontent.com/-WIuWgVcU3Qw/URqubRVcj4I/AAAAAAAAAbs/YvbwgGjwdIQ/s160-c/Antelope%252520Walls.jpg", "https://lh6.googleusercontent.com/-UBmLbPELvoQ/URqucCdv0kI/AAAAAAAAAbs/IdNhr2VQoQs/s160-c/Apre%2525CC%252580s%252520la%252520Pluie.jpg", "https://lh3.googleusercontent.com/-s-AFpvgSeew/URquc6dF-JI/AAAAAAAAAbs/Mt3xNGRUd68/s160-c/Backlit%252520Cloud.jpg", "https://lh5.googleusercontent.com/-bvmif9a9YOQ/URquea3heHI/AAAAAAAAAbs/rcr6wyeQtAo/s160-c/Bee%252520and%252520Flower.jpg", "https://lh5.googleusercontent.com/-n7mdm7I7FGs/URqueT_BT-I/AAAAAAAAAbs/9MYmXlmpSAo/s160-c/Bonzai%252520Rock%252520Sunset.jpg", "https://lh6.googleusercontent.com/-4CN4X4t0M1k/URqufPozWzI/AAAAAAAAAbs/8wK41lg1KPs/s160-c/Caterpillar.jpg", "https://lh3.googleusercontent.com/-rrFnVC8xQEg/URqufdrLBaI/AAAAAAAAAbs/s69WYy_fl1E/s160-c/Chess.jpg", "https://lh5.googleusercontent.com/-WVpRptWH8Yw/URqugh-QmDI/AAAAAAAAAbs/E-MgBgtlUWU/s160-c/Chihuly.jpg", "https://lh5.googleusercontent.com/-0BDXkYmckbo/URquhKFW84I/AAAAAAAAAbs/ogQtHCTk2JQ/s160-c/Closed%252520Door.jpg", "https://lh3.googleusercontent.com/-PyggXXZRykM/URquh-kVvoI/AAAAAAAAAbs/hFtDwhtrHHQ/s160-c/Colorado%252520River%252520Sunset.jpg", "https://lh3.googleusercontent.com/-ZAs4dNZtALc/URquikvOCWI/AAAAAAAAAbs/DXz4h3dll1Y/s160-c/Colors%252520of%252520Autumn.jpg", "https://lh4.googleusercontent.com/-GztnWEIiMz8/URqukVCU7bI/AAAAAAAAAbs/jo2Hjv6MZ6M/s160-c/Countryside.jpg", "https://lh4.googleusercontent.com/-bEg9EZ9QoiM/URquklz3FGI/AAAAAAAAAbs/UUuv8Ac2BaE/s160-c/Death%252520Valley%252520-%252520Dunes.jpg", "https://lh6.googleusercontent.com/-ijQJ8W68tEE/URqulGkvFEI/AAAAAAAAAbs/zPXvIwi_rFw/s160-c/Delicate%252520Arch.jpg", "https://lh5.googleusercontent.com/-Oh8mMy2ieng/URqullDwehI/AAAAAAAAAbs/TbdeEfsaIZY/s160-c/Despair.jpg", "https://lh5.googleusercontent.com/-gl0y4UiAOlk/URqumC_KjBI/AAAAAAAAAbs/PM1eT7dn4oo/s160-c/Eagle%252520Fall%252520Sunrise.jpg", "https://lh3.googleusercontent.com/-hYYHd2_vXPQ/URqumtJa9eI/AAAAAAAAAbs/wAalXVkbSh0/s160-c/Electric%252520Storm.jpg", "https://lh5.googleusercontent.com/-PyY_yiyjPTo/URqunUOhHFI/AAAAAAAAAbs/azZoULNuJXc/s160-c/False%252520Kiva.jpg", "https://lh6.googleusercontent.com/-PYvLVdvXywk/URqunwd8hfI/AAAAAAAAAbs/qiMwgkFvf6I/s160-c/Fitzgerald%252520Streaks.jpg", "https://lh4.googleusercontent.com/-KIR_UobIIqY/URquoCZ9SlI/AAAAAAAAAbs/Y4d4q8sXu4c/s160-c/Foggy%252520Sunset.jpg", "https://lh6.googleusercontent.com/-9lzOk_OWZH0/URquoo4xYoI/AAAAAAAAAbs/AwgzHtNVCwU/s160-c/Frantic.jpg", "https://lh3.googleusercontent.com/-0X3JNaKaz48/URqupH78wpI/AAAAAAAAAbs/lHXxu_zbH8s/s160-c/Golden%252520Gate%252520Afternoon.jpg", "https://lh6.googleusercontent.com/-95sb5ag7ABc/URqupl95RDI/AAAAAAAAAbs/g73R20iVTRA/s160-c/Golden%252520Gate%252520Fog.jpg", "https://lh3.googleusercontent.com/-JB9v6rtgHhk/URqup21F-zI/AAAAAAAAAbs/64Fb8qMZWXk/s160-c/Golden%252520Grass.jpg", "https://lh4.googleusercontent.com/-EIBGfnuLtII/URquqVHwaRI/AAAAAAAAAbs/FA4McV2u8VE/s160-c/Grand%252520Teton.jpg", "https://lh4.googleusercontent.com/-WoMxZvmN9nY/URquq1v2AoI/AAAAAAAAAbs/grj5uMhL6NA/s160-c/Grass%252520Closeup.jpg", "https://lh3.googleusercontent.com/-6hZiEHXx64Q/URqurxvNdqI/AAAAAAAAAbs/kWMXM3o5OVI/s160-c/Green%252520Grass.jpg", "https://lh5.googleusercontent.com/-6LVb9OXtQ60/URquteBFuKI/AAAAAAAAAbs/4F4kRgecwFs/s160-c/Hanging%252520Leaf.jpg", "https://lh4.googleusercontent.com/-zAvf__52ONk/URqutT_IuxI/AAAAAAAAAbs/D_bcuc0thoU/s160-c/Highway%2525201.jpg", "https://lh6.googleusercontent.com/-H4SrUg615rA/URquuL27fXI/AAAAAAAAAbs/4aEqJfiMsOU/s160-c/Horseshoe%252520Bend%252520Sunset.jpg", "https://lh4.googleusercontent.com/-JhFi4fb_Pqw/URquuX-QXbI/AAAAAAAAAbs/IXpYUxuweYM/s160-c/Horseshoe%252520Bend.jpg", "https://lh5.googleusercontent.com/-UGgssvFRJ7g/URquueyJzGI/AAAAAAAAAbs/yYIBlLT0toM/s160-c/Into%252520the%252520Blue.jpg", "https://lh3.googleusercontent.com/-CH7KoupI7uI/URquu0FF__I/AAAAAAAAAbs/R7GDmI7v_G0/s160-c/Jelly%252520Fish%2525202.jpg", "https://lh4.googleusercontent.com/-pwuuw6yhg8U/URquvPxR3FI/AAAAAAAAAbs/VNGk6f-tsGE/s160-c/Jelly%252520Fish%2525203.jpg", "https://lh5.googleusercontent.com/-GoUQVw1fnFw/URquv6xbC0I/AAAAAAAAAbs/zEUVTQQ43Zc/s160-c/Kauai.jpg", "https://lh6.googleusercontent.com/-8QdYYQEpYjw/URquwvdh88I/AAAAAAAAAbs/cktDy-ysfHo/s160-c/Kyoto%252520Sunset.jpg", "https://lh4.googleusercontent.com/-vPeekyDjOE0/URquwzJ28qI/AAAAAAAAAbs/qxcyXULsZrg/s160-c/Lake%252520Tahoe%252520Colors.jpg", "https://lh4.googleusercontent.com/-xBPxWpD4yxU/URquxWHk8AI/AAAAAAAAAbs/ARDPeDYPiMY/s160-c/Lava%252520from%252520the%252520Sky.jpg", "https://lh3.googleusercontent.com/-897VXrJB6RE/URquxxxd-5I/AAAAAAAAAbs/j-Cz4T4YvIw/s160-c/Leica%25252050mm%252520Summilux.jpg", "https://lh5.googleusercontent.com/-qSJ4D4iXzGo/URquyDWiJ1I/AAAAAAAAAbs/k2pBXeWehOA/s160-c/Leica%25252050mm%252520Summilux.jpg", "https://lh6.googleusercontent.com/-dwlPg83vzLg/URquylTVuFI/AAAAAAAAAbs/G6SyQ8b4YsI/s160-c/Leica%252520M8%252520%252528Front%252529.jpg", "https://lh3.googleusercontent.com/-R3_EYAyJvfk/URquzQBv8eI/AAAAAAAAAbs/b9xhpUM3pEI/s160-c/Light%252520to%252520Sand.jpg", "https://lh3.googleusercontent.com/-fHY5h67QPi0/URqu0Cp4J1I/AAAAAAAAAbs/0lG6m94Z6vM/s160-c/Little%252520Bit%252520of%252520Paradise.jpg", "https://lh5.googleusercontent.com/-TzF_LwrCnRM/URqu0RddPOI/AAAAAAAAAbs/gaj2dLiuX0s/s160-c/Lone%252520Pine%252520Sunset.jpg", "https://lh3.googleusercontent.com/-4HdpJ4_DXU4/URqu046dJ9I/AAAAAAAAAbs/eBOodtk2_uk/s160-c/Lonely%252520Rock.jpg", "https://lh6.googleusercontent.com/-erbF--z-W4s/URqu1ajSLkI/AAAAAAAAAbs/xjDCDO1INzM/s160-c/Longue%252520Vue.jpg", "https://lh6.googleusercontent.com/-0CXJRdJaqvc/URqu1opNZNI/AAAAAAAAAbs/PFB2oPUU7Lk/s160-c/Look%252520Me%252520in%252520the%252520Eye.jpg", "https://lh3.googleusercontent.com/-D_5lNxnDN6g/URqu2Tk7HVI/AAAAAAAAAbs/p0ddca9W__Y/s160-c/Lost%252520in%252520a%252520Field.jpg", "https://lh6.googleusercontent.com/-flsqwMrIk2Q/URqu24PcmjI/AAAAAAAAAbs/5ocIH85XofM/s160-c/Marshall%252520Beach%252520Sunset.jpg", "https://lh4.googleusercontent.com/-Y4lgryEVTmU/URqu28kG3gI/AAAAAAAAAbs/OjXpekqtbJ4/s160-c/Mono%252520Lake%252520Blue.jpg", "https://lh4.googleusercontent.com/-AaHAJPmcGYA/URqu3PIldHI/AAAAAAAAAbs/lcTqk1SIcRs/s160-c/Monument%252520Valley%252520Overlook.jpg", "https://lh4.googleusercontent.com/-vKxfdQ83dQA/URqu31Yq_BI/AAAAAAAAAbs/OUoGk_2AyfM/s160-c/Moving%252520Rock.jpg", "https://lh5.googleusercontent.com/-CG62QiPpWXg/URqu4ia4vRI/AAAAAAAAAbs/0YOdqLAlcAc/s160-c/Napali%252520Coast.jpg", "https://lh6.googleusercontent.com/-wdGrP5PMmJQ/URqu5PZvn7I/AAAAAAAAAbs/m0abEcdPXe4/s160-c/One%252520Wheel.jpg", "https://lh6.googleusercontent.com/-6WS5DoCGuOA/URqu5qx1UgI/AAAAAAAAAbs/giMw2ixPvrY/s160-c/Open%252520Sky.jpg", "https://lh6.googleusercontent.com/-u8EHKj8G8GQ/URqu55sM6yI/AAAAAAAAAbs/lIXX_GlTdmI/s160-c/Orange%252520Sunset.jpg", "https://lh6.googleusercontent.com/-74Z5qj4bTDE/URqu6LSrJrI/AAAAAAAAAbs/XzmVkw90szQ/s160-c/Orchid.jpg", "https://lh6.googleusercontent.com/-lEQE4h6TePE/URqu6t_lSkI/AAAAAAAAAbs/zvGYKOea_qY/s160-c/Over%252520there.jpg", "https://lh5.googleusercontent.com/-cauH-53JH2M/URqu66v_USI/AAAAAAAAAbs/EucwwqclfKQ/s160-c/Plumes.jpg", "https://lh3.googleusercontent.com/-eDLT2jHDoy4/URqu7axzkAI/AAAAAAAAAbs/iVZE-xJ7lZs/s160-c/Rainbokeh.jpg", "https://lh5.googleusercontent.com/-j1NLqEFIyco/URqu8L1CGcI/AAAAAAAAAbs/aqZkgX66zlI/s160-c/Rainbow.jpg", "https://lh5.googleusercontent.com/-DRnqmK0t4VU/URqu8XYN9yI/AAAAAAAAAbs/LgvF_592WLU/s160-c/Rice%252520Fields.jpg", "https://lh3.googleusercontent.com/-hwh1v3EOGcQ/URqu8qOaKwI/AAAAAAAAAbs/IljRJRnbJGw/s160-c/Rockaway%252520Fire%252520Sky.jpg", "https://lh5.googleusercontent.com/-wjV6FQk7tlk/URqu9jCQ8sI/AAAAAAAAAbs/RyYUpdo-c9o/s160-c/Rockaway%252520Flow.jpg", "https://lh6.googleusercontent.com/-6cAXNfo7D20/URqu-BdzgPI/AAAAAAAAAbs/OmsYllzJqwo/s160-c/Rockaway%252520Sunset%252520Sky.jpg", "https://lh3.googleusercontent.com/-sl8fpGPS-RE/URqu_BOkfgI/AAAAAAAAAbs/Dg2Fv-JxOeg/s160-c/Russian%252520Ridge%252520Sunset.jpg", "https://lh6.googleusercontent.com/-gVtY36mMBIg/URqu_q91lkI/AAAAAAAAAbs/3CiFMBcy5MA/s160-c/Rust%252520Knot.jpg", "https://lh6.googleusercontent.com/-GHeImuHqJBE/URqu_FKfVLI/AAAAAAAAAbs/axuEJeqam7Q/s160-c/Sailing%252520Stones.jpg", "https://lh3.googleusercontent.com/-hBbYZjTOwGc/URqu_ycpIrI/AAAAAAAAAbs/nAdJUXnGJYE/s160-c/Seahorse.jpg", "https://lh3.googleusercontent.com/-Iwi6-i6IexY/URqvAYZHsVI/AAAAAAAAAbs/5ETWl4qXsFE/s160-c/Shinjuku%252520Street.jpg", "https://lh6.googleusercontent.com/-amhnySTM_MY/URqvAlb5KoI/AAAAAAAAAbs/pFCFgzlKsn0/s160-c/Sierra%252520Heavens.jpg", "https://lh5.googleusercontent.com/-dJgjepFrYSo/URqvBVJZrAI/AAAAAAAAAbs/v-F5QWpYO6s/s160-c/Sierra%252520Sunset.jpg", "https://lh4.googleusercontent.com/-Z4zGiC5nWdc/URqvBdEwivI/AAAAAAAAAbs/ZRZR1VJ84QA/s160-c/Sin%252520Lights.jpg", "https://lh4.googleusercontent.com/-_0cYiWW8ccY/URqvBz3iM4I/AAAAAAAAAbs/9N_Wq8MhLTY/s160-c/Starry%252520Lake.jpg", "https://lh3.googleusercontent.com/-A9LMoRyuQUA/URqvCYx_JoI/AAAAAAAAAbs/s7sde1Bz9cI/s160-c/Starry%252520Night.jpg", "https://lh3.googleusercontent.com/-KtLJ3k858eY/URqvC_2h_bI/AAAAAAAAAbs/zzEBImwDA_g/s160-c/Stream.jpg", "https://lh5.googleusercontent.com/-dFB7Lad6RcA/URqvDUftwWI/AAAAAAAAAbs/BrhoUtXTN7o/s160-c/Strip%252520Sunset.jpg", "https://lh5.googleusercontent.com/-at6apgFiN20/URqvDyffUZI/AAAAAAAAAbs/clABCx171bE/s160-c/Sunset%252520Hills.jpg", "https://lh4.googleusercontent.com/-7-EHhtQthII/URqvEYTk4vI/AAAAAAAAAbs/QSJZoB3YjVg/s160-c/Tenaya%252520Lake%2525202.jpg", "https://lh6.googleusercontent.com/-8MrjV_a-Pok/URqvFC5repI/AAAAAAAAAbs/9inKTg9fbCE/s160-c/Tenaya%252520Lake.jpg", "https://lh5.googleusercontent.com/-B1HW-z4zwao/URqvFWYRwUI/AAAAAAAAAbs/8Peli53Bs8I/s160-c/The%252520Cave%252520BW.jpg", "https://lh3.googleusercontent.com/-PO4E-xZKAnQ/URqvGRqjYkI/AAAAAAAAAbs/42nyADFsXag/s160-c/The%252520Fisherman.jpg", "https://lh4.googleusercontent.com/-iLyZlzfdy7s/URqvG0YScdI/AAAAAAAAAbs/1J9eDKmkXtk/s160-c/The%252520Night%252520is%252520Coming.jpg", "https://lh6.googleusercontent.com/-G-k7YkkUco0/URqvHhah6fI/AAAAAAAAAbs/_taQQG7t0vo/s160-c/The%252520Road.jpg", "https://lh6.googleusercontent.com/-h-ALJt7kSus/URqvIThqYfI/AAAAAAAAAbs/ejiv35olWS8/s160-c/Tokyo%252520Heights.jpg", "https://lh5.googleusercontent.com/-Hy9k-TbS7xg/URqvIjQMOxI/AAAAAAAAAbs/RSpmmOATSkg/s160-c/Tokyo%252520Highway.jpg", "https://lh6.googleusercontent.com/-83oOvMb4OZs/URqvJL0T7lI/AAAAAAAAAbs/c5TECZ6RONM/s160-c/Tokyo%252520Smog.jpg", "https://lh3.googleusercontent.com/-FB-jfgREEfI/URqvJI3EXAI/AAAAAAAAAbs/XfyweiRF4v8/s160-c/Tufa%252520at%252520Night.jpg", "https://lh4.googleusercontent.com/-vngKD5Z1U8w/URqvJUCEgPI/AAAAAAAAAbs/ulxCMVcU6EU/s160-c/Valley%252520Sunset.jpg", "https://lh6.googleusercontent.com/-DOz5I2E2oMQ/URqvKMND1kI/AAAAAAAAAbs/Iqf0IsInleo/s160-c/Windmill%252520Sunrise.jpg", "https://lh5.googleusercontent.com/-biyiyWcJ9MU/URqvKculiAI/AAAAAAAAAbs/jyPsCplJOpE/s160-c/Windmill.jpg", "https://lh4.googleusercontent.com/-PDT167_xRdA/URqvK36mLcI/AAAAAAAAAbs/oi2ik9QseMI/s160-c/Windmills.jpg", "https://lh5.googleusercontent.com/-kI_QdYx7VlU/URqvLXCB6gI/AAAAAAAAAbs/N31vlZ6u89o/s160-c/Yet%252520Another%252520Rockaway%252520Sunset.jpg", "https://lh4.googleusercontent.com/-e9NHZ5k5MSs/URqvMIBZjtI/AAAAAAAAAbs/1fV810rDNfQ/s160-c/Yosemite%252520Tree.jpg", }; } main.xml如下: <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <GridView android:id="@+id/gridView" android:layout_width="match_parent" android:layout_height="wrap_content" android:columnWidth="90dip" android:gravity="center" android:numColumns="auto_fit" android:stretchMode="columnWidth" android:verticalSpacing="10dip" /> </RelativeLayout> gridview_item.xml如下: <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" android:layout_height="wrap_content" > <ImageView android:id="@+id/imageView" android:layout_width="90dip" android:layout_height="90dip" android:src="@drawable/default_image" android:layout_centerInParent="true" /> </RelativeLayout>  
文章
缓存  ·  Android开发
2014-01-16
...
跳转至:
开发与运维
5775 人关注 | 133431 讨论 | 319203 内容
+ 订阅
  • yii2.0的命名规范是怎样的?
  • Kubuntu(Ubuntu) 22.04安装OBS Studio
  • 硬核测评!三款开发者常用的主机远程管理软件
查看更多 >
数据库
252941 人关注 | 52312 讨论 | 99148 内容
+ 订阅
  • 设计模式 | 工厂(方法)模式
  • 如何实现对 Oracle 的实时数据捕获和性能调优|Flink CDC 专题
  • 大数据&AI产品月刊【2023年3月】
查看更多 >
人工智能
2871 人关注 | 12391 讨论 | 102585 内容
+ 订阅
  • Kubuntu(Ubuntu) 22.04安装OBS Studio
  • 金融网络安全建设更难了?看一线从业者的“实战”经验分享丨2023 INSEC WORLD
  • chatBot对人工智能的影响
查看更多 >
安全
1243 人关注 | 24148 讨论 | 85773 内容
+ 订阅
  • 硬核测评!三款开发者常用的主机远程管理软件
  • 金融网络安全建设更难了?看一线从业者的“实战”经验分享丨2023 INSEC WORLD
  • chatBot对人工智能的影响
查看更多 >
大数据
188709 人关注 | 30983 讨论 | 83889 内容
+ 订阅
  • 金融网络安全建设更难了?看一线从业者的“实战”经验分享丨2023 INSEC WORLD
  • Flutter如何内存优化简单介绍
  • 当 ChatGPT 遇上开源容器安全工具集「问脉」
查看更多 >