月度归档:2019年05月

怎么学会拒绝

大多数时候,难以拒绝别人,其实是无法接受一个“不被别人喜爱的自己”——即是只有在不断满足对方的要求,并且在这个过程中判断自己被对方接纳的时候,才能获得被对方肯定的满足感。

尽管对方可能只是自己生活中一个过客,甚至只是一名不会对自己产生任何影响的推销员,我们都可能因为对方因我们的拒绝而产生的失望感到自责,所以,会通过满足对方的提议来实现对方的肯定。

但是,很多人没有意识到的是,这种满足感的来源,是你大量的时间耗费和情感耗费。

我认识一个朋友,她有一个好闺蜜在做代购。每一次,当她的好闺蜜代购时,都会让她帮忙转发,甚至要求她转发给其他好友,并截图给她看。

我那个朋友,还真的照做了。

我问,你难道不觉得这件事情很不值得吗?

她回答我说,是很不值得,但是她求我帮忙的时候,我真的不知道如何拒绝,我只能去做。

这就是典型的无法拒绝别人的情形。

当她在耗费大量时间、精力和人脉完成一件事情,并且潜意识中认为这件事非做不可的时候,其实是一种“瘾”——这种瘾所产生的心理暗示强迫你“必须完成对方要求你的事情”。

本质上它和游戏成瘾没有什么两样。

很多时候难以拒绝某一个人,不是因为TA和自己有多熟,而是他的举动让你产生了“上瘾感”:

  1. TA往往会把这件事情形容得对TA很重要,并且用上一些优先级很高的词,比如“求你了”“拜托了”这样的说辞;
  2. TA会在你没有帮助TA之前,表现出“你这样做很不够意思”的态度;并且在你帮助TA之后,告诉你“真的很感谢你”——但实际上,你们的感情并不会因为你帮了TA而更加坚固,他还会再次请你帮忙,然后再次“真的很感谢你”。

如果你面对以上这种举动,自己无法抗拒的话,那么基本可以判断你有这种成瘾行为。

戒掉任何一种“瘾”,最好的办法,是找到另一种“瘾”替代它。

这听上去好像有点荒唐,但是却是最好的办法。

其实任何一个难以拒绝别人的人,都或多或少有讨好型人格的一面——在这一面人格里,内心会不断告诉你,别人对自己的喜爱很重要,因此照顾别人的情绪很重要。

因此,你可以找到一种更强烈的瘾,比如说,去讨好更重要的人。

所谓去讨好更重要的人,就是你反复地告诉自己,讨好真正喜爱自己的人,比讨好一个只会索取的人,要更有价值。

最重要的一点是,当你不断固化“讨好只会索取的人”会使得你“不能照顾到更重要的人”的时候,你的大脑就会自动在这两类成瘾对象里面比较并选择更重要的人,而不再去选择讨好那些只会索取的人。

比如当一个根本和你不熟的同事平白无故就要求你帮她做ppt,但是你不好拒绝的时候,不妨这样思考:

如果我帮你做PPT,那也就意味着我少了和男/女朋友煲电话的两个小时。

难道帮一个陌生人,比我的伴侣的爱还要重要?

这样,你就可以从“讨好所有人”的上瘾者,变成“讨好爱自己和自己爱的人”的上瘾者。

而对爱自己和自己爱的人,给予他们关怀,不是很美妙的事么?

附:在言语上应该如何拒绝?

1. 当对方提出要求时,首先条件反射性地判断,“他是不是一个我值得帮忙的人”,以及“这件事是不是不值得的”。

2. 当做出这种判断确认不值得帮之后,回复对方:不好意思,我有更重要的事情要忙;

3. 在2的情形下,对方往往会从阐释客观事件(“我的这件事本身很容易的,你就帮一下”)和怀疑主观动机(“我看你也不忙啊”)来继续提出请求;

4. 如果对方阐释客观事件,需要做的不是和他辩论他让你帮忙的事到底是不是小事,而是让对方知道“既然是一件小事,那你完全可以自己做,或者有比我更好的人选代替我。”

5. 如果对方怀疑主观动机,需要做的不是告诉他“我真的很忙”,而是让对方知道,“我并没有义务帮忙,因此,无论我主观动因是什么,当我告诉你我很忙的时候,就代表我不想帮。在这种情况下,怀疑我的主观动因没有意义”。

6. 至于对方面对面要求你帮忙时,回复“让我考虑一下”,而不是含含糊糊地答应下来更有效——要知道,一旦你含含糊糊答应下来,你又会陷入犯瘾的困境:因为你潜意识中认为你已经获取到了对方的信任,因此必须有义务完成。

因此,回复“考虑一下”,当大脑完成判断“这个人是不是一个只会索取的人”之后,再回复TA(这样也可以使用电话或短信,避免尴尬),这样,你才能把时间留给真正值得的人。

7. 一般而言,熟练使用语言来请求别人帮助的人,对肢体语言也更为敏感。如果你一副纠结万分的样子,对方会意识到一旦TA多说点好话,你就会被他说服;因此,当你确定不想帮助他之后,不妨下意识采用一些强势防御性的肢体语言,如双臂抱在胸前(而不是纠结万分的样子),对方更容易知难而退。

PS:这篇如何拒绝别人指南,只是为了让更多想要拒绝但苦恼于“无法说不”的人,能够把时间留给更重要的人,而绝对不是告诉大家“如何保持冷漠.jpg”。

接受而不是拒绝那些值得爱的人,本来就是生命中最好的事。

更多可能对你有帮助的回答:

来源:  https://www.zhihu.com/question/19897774

------------

另外一个有用的 学会拒绝:

学会说“不”是一种智慧。生活中有的人不懂得拒绝诱惑,不懂得拒绝别人的要求,结果弄的生活一团糟。合理地拒绝一些东西,才能得到更好的东西。以下分享一些经验,教你如何学会拒绝。

工具/原料

  • 心态,信念

方法/步骤

  1. 拒绝诱惑。

    开车的人拒绝美酒的诱惑,得到的是安全。学习的的人拒绝玩乐的诱惑,得到的是好成绩。生活中有各种诱惑,在诱惑面前,要懂得合理地拒绝。拒绝的实质是一种否定性的选择,拒绝掉不好的,选择合理的。在诱惑面前,要有坚定的意志,要勇敢地说“不”。

  2. 2

    力所能及。

    认清自己的实力,能力不及的事要及时拒绝。这是负责任的表现,不是软弱。有责任感的人会为自己所做的事负责,选择力所能及的事,拒绝做不到的事。

  3. 3

    有理有节。

    当一个人的尊严受到侵犯时,只有适当的回击。敢于拒绝,才能得到应有的尊重。该争时争,该退时退,做到有理有节,将会受益无穷。

  4. 4

    面对不合理要求,学会拒绝。

    有的人是老好人,乐于助人本没有错,但超出能力范围,结果只会适得其反。面对别人的不合理要求,要学会拒绝,一味地容忍退让,不只自己受累,也是对别人不负责任。

  5. 5

    拒绝浪费时间。

    鲁迅说“时间就像海绵里的水,只要愿意挤,总还是有的。”浪费时间就是浪费生命。拒绝浪费时间就等于对生命的珍惜。中外的名人为了更好的工作和学习,想出了很多有趣的谢客方法。启蒙思想家伏尔泰用装病来挡驾。革命家邓中夏在北大读书时,为不受干扰,写了“五分钟谈话”的纸条贴在书桌上。拒绝浪费时间,获得的是更多时间工作和学习。

  6. 6

    平和和冷静。

    不要愤怒拒绝,平和和冷静是最能够化解矛盾的。俗话说“伸手不打笑脸人。”当你拒绝时,不要用愤怒的态度,应该尽量平和地拒绝。这样对方也容易更好地接受。有时良好的态度是解决问题的关键因素。

     

  7. 7

    拒绝时兼顾情谊与爱。

    不要无情拒绝,原则和立场很重要,然而情谊与爱同样很重要。当拒绝朋友不合理的要求时,要让对方有台阶可下,在心理和情感上接受,不失为一种高明的方法。

注意事项

  • 情谊,平和
来源: https://jingyan.baidu.com/article/020278116dd4831bcc9ce5f2.html
-----------------------
人生需要学会拒绝

不知从何时起,乖顺的性格总会让我在一切不乐意的事情面前违拗自己的个性说:“好”,虽然打心底是并不想去做的,但是出于维护好人情世故,我一般都会顺从。而这种事情的一般结果就是:我去做了,可是我并没有很享受这个过程,反而,在配合的过程中,我会显得焦躁不安,甚至并不配合。

那么问题来了:对于自己并不喜欢的事情,要不要试着拒绝?

在很多次的“陪练”之后,对于“人情”和“心性”两难齐全的问题,我觉得还是要自己试着去拒绝。我要说的拒绝,不是因为拒绝而拒绝,而是自己确实不想违拗自己的个性去配合的东西,因为自己确实也配合不来。更重要的是,我想让自己学会说不。

从小到大,我们对于父母、长辈的安排总是言听计从,小到该穿什么衣服,该留什么头型儿,基本都是出于父母的意愿,我们很少有能够表达自己意见的场合和机会。即使有,也会被当成小儿胡言,被随便应付过去。一旦,当我们明确的表达出不乐意的意思时,便会被冠上“没有礼貌,没有远见”之类的名号,被加以打压。于是,即便发表了意见也不会有人重视和采纳,何必又再发表呢?所以,中国孩子在表达自己意愿的欲望方面也越来越弱,跟着前辈的建议走,只要自己没有反感到极致便也一直隐忍。于是,不满的情绪也会一直酝酿,如果没有一个良好的渠道加以泄出的话,很容易积压不满而导致最终的崩溃和爆发。

很多人会说,很多情况下的拒绝是“没有礼貌的,甚至是情商巨低的表现。”拒绝并不是无礼的,其中必须要讲究拒绝的艺术,如何有礼貌的拒绝,委婉的表达出自己的意思,这才是人际交往的中的重要一课。但是,基本就是如何得体的表达出来自己的拒绝之意,同时又能顾忌到对方的情绪,这正是一门技术活儿。

可能是从小过于乖顺,连“不”我都很少会说出口,从觉得一旦拒绝便会影响到双方关系,不能够再做好朋友。另一方面,拒绝是弱势的我,也从小受到了苦头,不得不去做很多自己其实并不乐意或者并不擅长的事物。在经过很多次的教训以后,我开始反思,如何才能够教自己学会说“不”?如何才能够得体的拒绝对方?

拒绝对方,总需要一个合理的理由,有时理由是正当成立的,有的理由则是自己为了拒绝别人而刻意伪造的。对于一个并不善于或者懒得说谎的人来说,假借借口对于我,会显得是一件勉为其难的事情。于是乎,对于我而言能回避则尽量回避,譬如:尽量会不接这种要求的来电,或者刻意避开这种有整机意愿的场合。因为不敢于直接拒绝对方,或者没有好的办法可以委婉的拒绝他人,于是老是采用回避的办法。因为不想面对这种场合,所以尽量躲开,就好像消失了一样。其实,这种办法是拒绝当中最最低端的,因为没有表明自己的态度,或者不敢表明自己的态度(因为害怕直接拒绝别人的不良后果,比如影响到上下级关系,朋友之间的关系)而刻意回避别人的诉求,反而会给别人“拒人千里之外”的冷漠形象。相反,如果能够从双方的利益角度出发,客观温和的陈述出自己的意见和要求,可能最后的拒绝也可以取得对方的谅解,从而也实现了自己“礼貌地拒绝他人”的目的。

在实际操作中,需要注意到的是,表达拒绝的愿望是,能够真诚恳切的表达出自己的想法,即是对方没有立即在心理上取得谅解,在以后的交往过程中也不会造成“没有礼数”的不良印象。

其次,如果过于在乎别人对于自己的评价和看法,也很容易导致在人际关系中的失衡。尽管在当下,我们必须特别注重别人的感受,但人际交往毕竟还是一件以“我”为主的事情,如果自己因为太过于重视别人的意见和想法,很容易就会被别人的意见左右,甚至被牵着鼻子走,在不触及原则的前提之下,首先“以我为主”,其次能够采纳别人好的建议和想法,这才是稳定健全型人格应该具有的气质:理性、客观、有自己的看法和见解,积极包容吸取,“以我为主”,同时又保留了自己的开放性。当然,这说来都是比较轻松的,但是在具体的操作中还有赖于自身不断的调整和琢磨。但是总体的原则基本还是要“以我为主”,这也是保留自己个性和独立性的一种方式。

拒绝,其实在我看来,就是如何平衡好做自己和迁就他人的微妙关系。如果,在交往当中发现自己也能够接受,那么可以试着去适应别人。相反,如果因为不会拒绝,而导致自己在人际交往过程中背上了沉重的心理包袱,那么就要学着试着拒绝。毕竟,我相信做自己始终才是一件最舒服的事情。

来源:  https://www.douban.com/note/508229901/

如何清理Docker占用的磁盘空间

【编者的话】用了Docker,好处挺多的,但是有一个不大不小的问题,它会一不小心占用太多磁盘,这就意味着我们必须及时清理。

作为一个有信仰的技术公司,我们Fundebug的后台采用了酷炫的全Docker化架构,所有服务,包括数据库都运行在Docker里面。这样做当然不是为了炫技,看得清楚的好处还是不少的:
  • 所有服务器的配置都非常简单,只安装了Docker,这样新增服务器的时候要简单很多。
  • 可以非常方便地在服务器之间移动各种服务,下载Docker镜像就可以运行,不需要手动配置运行环境。
  • 开发/测试环境与生产环境严格一致,不用担心由于环境问题导致部署失败。

至少,上线这一年多来,Docker一直非常稳定,没有出什么问题。但是,它有一个不大不小的问题,会比较消耗磁盘空间。

如果Docker一不小心把磁盘空间全占满了,你的服务也就算玩完了,因此所有Docker用户都需要对此保持警惕。当然,大家也不要紧张,这个问题还是挺好解决的。

1. Docker System命令

在《谁用光了磁盘?Docker System命令详解》中,我们详细介绍了Docker System命令,它可以用于管理磁盘空间。

docker system df命令,类似于Linux上的df命令,用于查看Docker的磁盘使用情况:

docker system df
TYPE                TOTAL               ACTIVE              SIZE                RECLAIMABLE
Images              147                 36                  7.204GB             3.887GB (53%)
Containers          37                  10                  104.8MB             102.6MB (97%)
Local Volumes       3                   3                   1.421GB             0B (0%)
Build Cache                                                 0B                  0B

可知,Docker镜像占用了7.2GB磁盘,Docker容器占用了104.8MB磁盘,Docker数据卷占用了1.4GB磁盘。

docker system prune命令可以用于清理磁盘,删除关闭的容器、无用的数据卷和网络,以及dangling镜像(即无tag的镜像)。docker system prune -a命令清理得更加彻底,可以将没有容器使用Docker镜像都删掉。注意,这两个命令会把你暂时关闭的容器,以及暂时没有用到的Docker镜像都删掉了……所以使用之前一定要想清楚吶。

执行docker system prune -a命令之后,Docker占用的磁盘空间减少了很多:

docker system df
TYPE                TOTAL               ACTIVE              SIZE                RECLAIMABLE
Images              10                  10                  2.271GB             630.7MB (27%)
Containers          10                  10                  2.211MB             0B (0%)
Local Volumes       3                   3                   1.421GB             0B (0%)
Build Cache                                                 0B                  0B

2. 手动清理Docker镜像/容器/数据卷

对于旧版的Docker(版本1.13之前),是没有Docker System命令的,因此需要进行手动清理。这里给出几个常用的命令:

删除所有关闭的容器

docker ps -a | grep Exit | cut -d ' ' -f 1 | xargs docker rm

删除所有dangling镜像(即无tag的镜像)

docker rmi $(docker images | grep "^<none>" | awk "{print $3}")

删除所有dangling数据卷(即无用的Volume)

docker volume rm $(docker volume ls -qf dangling=true)

3. 限制容器的日志大小

有一次,当我使用1与2提到的方法清理磁盘之后,发现并没有什么作用,于是,我进行了一系列分析。

在Ubuntu上,Docker的所有相关文件,包括镜像、容器等都保存在/var/lib/docker/目录中:

du -hs /var/lib/docker/
97G /var/lib/docker/

Docker竟然使用了将近100GB磁盘,这也是够了。使用du命令继续查看,可以定位到真正占用这么多磁盘的目录:

92G  /var/lib/docker/containers/a376aa694b22ee497f6fc9f7d15d943de91c853284f8f105ff5ad6c7ddae7a53

docker ps可知,Nginx容器的ID恰好为a376aa694b22,与上面的目录/var/lib/docker/containers/a376aa694b22的前缀一致:

docker ps
CONTAINER ID        IMAGE                                       COMMAND                  CREATED             STATUS              PORTS               NAMES
a376aa694b22        192.168.59.224:5000/nginx:1.12.1            "nginx -g 'daemon off"   9 weeks ago         Up 10 minutes                           nginx

因此,Nginx容器竟然占用了92GB的磁盘。进一步分析可知,真正占用磁盘空间的是Nginx的日志文件。那么这就不难理解了。我们Fundebug每天的数据请求为百万级别,那么日志数据自然非常大。

使用truncate命令,可以将Nginx容器的日志文件“清零”:

truncate -s 0 /var/lib/docker/containers/a376aa694b22ee497f6fc9f7d15d943de91c853284f8f105ff5ad6c7ddae7a53/*-json.log

当然,这个命令只是临时有作用,日志文件迟早又会涨回来。要从根本上解决问题,需要限制Nginx容器的日志文件大小。这个可以通过配置日志的max-size来实现,下面是Nginx容器的docker-compose配置文件:

nginx:
image: nginx:1.12.1
restart: always
logging:
driver: "json-file"
options:
  max-size: "5g"

重启Nginx容器之后,其日志文件的大小就被限制在5GB,再也不用担心了~

4. 重启Docker

还有一次,当我清理了镜像、容器以及数据卷之后,发现磁盘空间并没有减少。根据Docker disk usage提到过的建议,我重启了Docker,发现磁盘使用率从83%降到了19%。根据高手指点,这应该是与内核3.13相关的Bug,导致Docker无法清理一些无用目录:

it's quite likely that for some reason when those container shutdown, docker couldn't remove the directory because the shm device was busy. This tends to happen often on 3.13 kernel. You may want to update it to the 4.4 version supported on trusty 14.04.5 LTS.

The reason it disappeared after a restart, is that daemon probably tried and succeeded to clean up left over data from stopped containers.

我查看了一下内核版本,发现真的是3.13:

uname -r
3.13.0-86-generic

如果你的内核版本也是3.13,而且清理磁盘没能成功,不妨重启一下Docker。当然,这个晚上操作比较靠谱。

参考

  • 谁用光了磁盘?Docker System命令详解
  • INTRODUCING DOCKER 1.13
  • Docker文档:docker system
  • Docker文档:json-file
  • Docker disk usage

DRM之Widevine学习入门

中国的网络视频行业高速增长,收入已突破千亿元。然而网络盗版猖獗,造成的损失已超过正版收入的一半,版权保护的重要性与日俱增。

现有方案的问题

解决盗版问题的一般思路是对视频加密,仅允许授权用户解密播放视频。常见的视频加密方案有以下两类:

  • 自研加解密方案:业务自行研发的非通用加解密方案(如调换部分帧的位置)。
  • 通用 AES 加解密方案:使用 Dash 的 ClearKey 或 HLS 的普通 AES 加密。

这两种类型方案都有各自的问题与缺陷。

方案类型问题与缺陷
自研加解密方案
  • 自研加解密方案开发难度大
  • 自定义加解密算法安全性难以保证
  • 难以在浏览器、OTT 等通用平台播放
  • 解码内容有被转录风险
  • 无权发布知名版权方(好莱坞、迪士尼等)的内容
通用 AES 加解密方案
  • 密钥明文暴露易遭窃取
  • 获取密钥地址易遭篡改
  • 无法指定密钥有效期

商业级 DRM

商业级 DRM 是一类利用 License 实现高安全级别的版权保护系统。终端播放视频时,必须先获取 License(License 中包含了解密密钥、密钥有效期及终端信息等),然后使用 License 中的密钥解密播放视频。

使用商业级 DRM 保护视频,有以下优势:

  • 视频内容加密:采用 CENC 对内容加密,播放需要密钥解密。
  • 密钥不可见:密钥本身被加密,仅 OS 中的解密模块能读取密钥。
  • License 终端绑定:License 仅对单个终端有效,其他终端无法使用。
  • License 支持过期:支持指定 License 的有效期。
  • 解码过程安全:支持硬件级(TEE)解密和解码。
  • 知名版权方认证:好莱坞、迪士尼认证。

目前主流的 DRM 系统有 Widevine 和 FairPlay 两种:

DRM 类型适用文件格式适用播放环境
WidevineDashAndriod 播放器、Chrome 和 Firefox 浏览器
FairPlayHLSiOS 播放器以及 Safari 浏览器

  1. App 后台 上传视频(服务端 API 或控制台),同时指定上传后执行带有 视频加密 任务的任务流。
  2. 视频处理模块执行视频加密任务,加密后的内容写入到 COS 存储,通知 App 后台视频加密任务完成。
  3. App 终端向 App 后台请求要播放的视频,后台对终端用户进行鉴权(如是否付费),鉴权通过后派发视频对应的 Token 作为授权播放的凭证。
  4. 播放器下载从点播 CDN 下载加密的视频内容。
  5. 播放器向点播请求 License,点播对 Token 鉴权,通过后派发 License,播放器使用 License 解密播放视频内容。

用户鉴权与播放授权

App 后台获取到视频上传和加密完成的通知后,即可在 App 的视频列表中加入该视频。当 App 终端请求播放这个视频时,后台需要对终端做如下校验:

  • 播放请求是否来自于 App 的合法用户。
  • 该用户是否已付费观看该视频。

校验通过后,App 后台对用户进行授权,即向终端返回 Token(计算方式参见 Token 生成)。终端播放对应 FileId 的视频时,超级播放器 SDK 使用 Token 获取到解密视频需要的 License,完成对视频的解密播放。

播放 DRM 加密的视频

数字版权管理解决方案(Digital Rights Management,DRM),通过技术手段加密内容,用来控制带版权作品的使用、更改和分发,保护带版权内容的安全。适用于音乐、电影等带版权的多媒体内容。

Android 播放器可以播放两种方式加密的输出:

  1. 基于 Widevine 加密的 Dash 格式
  2. 基于 SimpleAES 加密的 HLS 格式

关于 DRM 的更多详情,您可以参考 如何对内容做版权保护 方案文档。

Android中的DRM软件架构:

Widevine是google推出的一种DRM,支持从google指定的服务器上,下载经google加密的版权文件,如视频、应用等。

Widevine DRM解决方案结合以下行业标准,提供强大的多平台内容保护:

基于HTTP的动态自适应流技术(DASH) 
DASH采用标准HTTP 协议,厂商可在现有网络基础设施(网络服务器、内容分发网络、防火墙等)上方便的实施DASH技术。

DASH为不同DRM系统构建了一个简单有效的系统,以共享密钥,密钥标识符,加密算法,参数和信令,以及将专有数据存储在保护系统特定报头(PSSH)中的位置,但将DRM具体实现留给了系统自身。它为ECM中的每个DRM存储此信息 - 这使得每个DRM能够将其自己的健壮性规则,安全性实施和密钥交换系统应用于相同内容文件的加密内容。通用加密解决方案CENC允许内容提供商每个容器/编解码器加密和打包一次内容,并将其与各种密钥系统,CDM和客户端一起使用:即任何支持公共加密的CDM。例如,使用Playready打包的视频可以使用Widevine CDM在浏览器中播放,从Widevine许可服务器获取密钥。

加密媒体扩展(Encrypted Media Extensions,EME)是 W3C 提出的一种规范,用于在 Web 浏览器和 DRM 代理软件之间提供通信通道。EME提供了一个API,使Web应用程序能够与内容保护系统进行交互,以允许播放加密的音频和视频。EME旨在使相同的应用程序和加密文件能够在任何浏览器中使用,而不管底层保护系统如何。前者是通过标准化的API和流程实现的,而后者是通过公共加密的概念实现的。

媒体源扩展(Media Source Extensions, MSE)是一项 W3C 规范,扩展了HTMLMediaElement,支持HTML5视频和音频,允许 JavaScript 生成媒体流以支持回放。这可以用于自适应流(adaptive streaming)及随时间变化的视频直播流(live streaming)等应用场景。

内容解密模块(Content Decryption Module, CDM)

CENC 声明了一套标准的加密和密钥映射方法,它可用于在多个 DRM 系统上解密相同的内容,只需要提供相同的密钥即可。DRM 提供商(例如,EME 可用于 Edge 平台上的 Playready 和 Chrome 平台上的 Widewine)拥有一套通用的 API,这些 API 能够从 DRM 授权模块读取视频内容加密密钥用于解密。在浏览器内部,基于视频内容的元信息,EME 可以通过识别它使用了哪个 DRM 系统加密,并调用相应的解密模块(Content Decryption Module, CDM)解密 CENC 加密过的内容。解密模块 CDM 则会去处理内容授权相关的工作,获得密钥并解密视频内容。CENC 没有规定授权的发放、授权的格式、授权的存储、以及使用规则和权限的映射关系等细节,这些细节的处理都由 DRM 提供商负责。

Widevine Classic是Google专有的DRM方案,用于直播,点播和下载内容。 它要求内容以Google自己的特定格式打包,Widevine Classic已被Widevine Modular取代。与Widevine Classic一起使用的视频内容格式为:Widevine(.WVM)。Widevine Modular支持MPEG-DASH,CMAF,HLS和Smooth Streaming ABR视频格式,以及公共加密(CENC)等开放标准。
Widevine 测试APK:WidevineSamplePlayer.apk 测试Widevine Classic,ExoPlayerDemo.apk 测试Widevine Modular

Widevine安全级别

安全级别安全BootloaderWidevine密钥配置安全硬件或ARM Trust ZoneWidevine密钥箱和视频密钥处理硬件视频路径
1级工厂提供Widevine Keys密钥永远不会暴露给主机CPU硬件保护的视频路径
2级工厂提供Widevine Keys密钥永远不会暴露给主机CPU硬件保护的视频路径
3级是*现场提供Widevine Keys没有清除暴露给主机CPU的密钥清晰的视频流传送到视频解码器

手机厂商可以通过Google的授权以获取Widevine DRM对应的软件包从而将Widevine DRM集成到自己的产品中。

Widevine DRM代码结构大体可分为三部分:

1、Android中的基本框架。包括WVMExtractor等。这部分代码在AOSP(Android 开放源代码项目)中可以看到。主要功能实现封装在Widevine专利代码包。

2、Widevine专利代码包。需要得到Google授权才能得到。该包提供了很多Widevine专用库用于完成Widevine DRM权限检查、解密。它还提供了一些简单App用于测试。

3、手机厂商自身的安全认证。Widevine支持在硬件层与厂商的安全机制绑定,在boot等底层中加入自主研发的安全机制,只有通过可信赖的bootloader才能使用具有正常权限的手机软件,并支持Widevine机制。

Android系统中的Widevine Crypto插件:

安全加密技术概念拓展:

ARM TrustZone® 技术是系统范围的安全方法,此系统方法意味着可以保护安全内存、加密块、键盘和屏幕等外设,从而可确保它们免遭软件攻击。处理器架构上,TrustZone将每个物理核虚拟为两个核,一个非安全核(Non-secure Core, NS Core)安全核(Secure Core)。两个虚拟的核以基于时间片的方式运行,根据需要实时占用物理核,并通过Monitor Mode在安全世界和非安全世界之间切换,Trustzone下的Monitor Mode实现了同一CPU上两个操作系统间的切换。逻辑上,安全世界中,安全系统的OS提供统一的服务,针对不同的安全需求加载不同的安全应用TA(Trusted Application)。

系统上电复位后,先从安全世界开始执行。安全世界会对非安全世界的bootloader进行验证,确保非安全世界执行的代码经过授权而没有被篡改过。然后非安全世界的bootloader会加载非安全世界的OS,完成整个系统的启动。在非安全系统的bootloader加载OS时,仍然需要安全世界对OS的代码进行验证,确保没有被篡改。基于安全考虑,各家TrustZone都实行闭源。

来源: 互联网搜集整理

android bitmap的缓存策略

在这篇文章中:

  • 1.lrucache
  • 2.disklrucache
  • 3. 缓存策略对比与总结

不论是android还是ios设备,流量对于用户而言都是宝贵的。在没有wifi的场景下,如果加载批量的图片消耗用户过多流量,被其知晓,又要被念叨一波~

如何避免消耗过多的流量呢?当程序第一次从网络加载图片后,就将其缓存到移动设备上,这样再次使用这个图片时,就不用再次从网络上下载为用户节省了流量。

目前常用的一种缓存算法是lru(least recently used),它的核心思想是当缓存满了,会优先淘汰近期最少使用的缓存对象。采用lru算法的缓存有两种:lrucache和disklrucache,lrucache主要用于实现内存缓存,disklrucache则用于存储设备缓存。

1.lrucache

lrucache是api level 12提供的一个泛型类,它内部采用一个linkedhashmap以强引用的方式存储外界的缓存对象,提供了get和put方法来完成缓存的获取和添加操作,当缓存满了,lrucache会remove掉较早使用的缓存对象,然后再添加新的对象。

过去实现内存缓存的常用做法是使用softreference或者使用weakreference,但是并不推荐这种做法,从api level 9以后,gc强制回收掉soft、weak引用,从而导致这些缓存并没有任何效率的提升。

lrucache的实现原理:

根据lru的算法思想,我们需要一种数据结构来快速定位哪个对象是最近访问的,哪个对象是最长时间未访问的,lrucache选择的是linkedhashmap这个数据结构,它是一个双向循环链表。来瞅一眼linkedhashmap的构造函数:

/** 初始化linkedhashmap

     * 第一个参数:initialcapacity,初始大小

     * 第二个参数:loadfactor,负载因子=0.75f

     * 第三个参数:accessorder=true,基于访问顺序;accessorder=false,基于插入顺序<br/>**/

   public linkedhashmap(int initialcapacity, float loadfactor, boolean accessorder) {

       super(initialcapacity, loadfactor);

       init();

       this.accessorder = accessorder;

    }

所以在lrucache中应该选择accessorder = true,当我们调用put、get方法时,linkedhashmap内部会将这个item移动到链表的尾部,即在链表尾部是最近刚刚使用的item,链表头部就是最近最少使用的item。当缓存空间不足时,可以remove头部结点释放缓存空间。

下面举例lrucache的典型使用姿势

int maxmemory = (int) (runtime.getruntime().maxmemory() / 1024);

int cachesize = maxmemory / 8;

mmemorycache = new lrucache<string bitmap="">(cachesize) {

    @override

    protected int sizeof(string key, bitmap bitmap) {

        return bitmap.getrowbytes() * bitmap.getheight() / 1024;

    }

};

 <br/>// 向 lrucache 中添加一个缓存对象

private void addbitmaptomemorycache(string key, bitmap bitmap) {

    if (getbitmapfrommemcache(key) == null) {

        mmemorycache.put(key, bitmap);

    }

}



//获取一个缓存对象

private bitmap getbitmapfrommemcache(string key) {

    return mmemorycache.get(key);

}</string>

上述示例代码中,总容量的大小是当前进程的可用内存的八分之一(官方推荐是八分之一哈,你们可以自己视情况定),sizeof()方法计算了bitmap的大小,sizeof方法默认返回的是你缓存item数目,源码中直接return 1(这里的源码比较简单,可以自己看看~)。

如果你需要cache中某个值释放,可以重写entryremoved()方法,这个方法会在元素被put或者remove的时候调用,源码默认是空实现。重写entryremoved()方法还可以实现二级内存缓存,进一步提高性能。思路如下:重写entryremoved(),把删除掉的item,再次存入另一个linkedhashmap中。这个数据结构当做二级缓存,每次获得图片的时候,按照一级缓存 、二级缓存、sdcard、网络的顺序查找,找到就停止。

2.disklrucache

当我们需要存大量图片的时候,我们指定的缓存空间可能很快就用完了,lrucache会频繁地进行trimtosize操作将最近最少使用的数据remove掉,但是hold不住过会又要用这个数据,又从网络download一遍,为此有了disklrucache,它可以保存这些已经下载过的图片。当然,从磁盘读取图片的时候要比内存慢得多,并且应该在非ui线程中载入磁盘图片。disklrucache顾名思义,实现存储设备缓存,即磁盘缓存,它通过将缓存对象写入文件系统从而实现缓存效果。

ps: 如果缓存的图片经常被使用,可以考虑使用contentprovider。

disklrucache的实现原理:

lrucache采用的是linkedhashmap这种数据结构来保存缓存中的对象,那么对于disklrucache呢?由于数据是缓存在本地文件中,相当于是持久保存的一个文件,即使app kill掉,这些文件还在滴。so ,,,,, 到底是啥?disklrucache也是采用linekedhashmap这种数据结构,但是不够,需要加持buff

,日志文件。日志文件可以看做是一块“内存”,map中的value只保存文件的简要信息,对缓存文件的所有操作都会记录在日志文件中。

disklrucache的初始化:

下面是disklrucache的创建过程:

private static final long disk_cache_size = 1024 * 1024 * 50; //50mb 

    

    file diskcachedir = getdiskcachedir(mcontext, "bitmap");

    if (!diskcachedir.exists()) {

        diskcachedir.mkdirs();

    }



    if (getusablespace(diskcachedir) > disk_cache_size) {

        try {

            mdisklrucache = disklrucache.open(diskcachedir, 1, 1,

                    disk_cache_size);

        } catch (ioexception e) {

            e.printstacktrace();

        }

    }

瞅了一眼,可以知道重点在open()函数,其中第一个参数表示文件的存储路径,缓存路径可以是sd卡上的缓存目录,具体是指/sdcard/android/data/package_name/cache,package_name表示当前应用的包名,当应用被卸载后, 此目录会一并删除掉。如果你希望应用卸载后,这些缓存文件不被删除,可以指定sd卡上其他目录。第二个参数表示应用的版本号,一般设为1即可。第三个参数表示单个结点所对应数据的个数,一般设为1。第四个参数表示缓存的总大小,比如50mb,当缓存大小超过这个设定值后,disklrucache会清除一些缓存保证总大小不会超过设定值。

disklrucache的数据缓存与获取缓存:

数据缓存操作是借助disklrucache.editor类完成的,editor表示一个缓存对象的编辑对象。

new thread(new runnable() {  

    @override  

    public void run() {  

        try {  

            string imageurl = "http://d.url.cn/myapp/qq_desk/friendprofile_def_cover_001.png";  

            string key = hashkeyfordisk(imageurl);  //md5对url进行加密,这个主要是为了获得统一的16位字符

            disklrucache.editor editor = mdisklrucache.edit(key);  //拿到editor,往journal日志中写入dirty记录

            if (editor != null) {  

                outputstream outputstream = editor.newoutputstream(0);  

                if (downloadurltostream(imageurl, outputstream)) {  //downloadurltostream方法为下载图片的方法,并且将输出流放到outputstream

                    editor.commit();  //完成后记得commit(),成功后,再往journal日志中写入clean记录

                } else {  

                    editor.abort();  //失败后,要remove缓存文件,往journal文件中写入remove记录

                }  

            }  

            mdisklrucache.flush();  //将缓存操作同步到journal日志文件,不一定要在这里就调用

        } catch (ioexception e) {  

            e.printstacktrace();  

        }  

    }  

}).start();

上述示例代码中,每次调用edit()方法时,会返回一个新的editor对象,通过它可以得到一个文件输出流;调用commit()方法将图片写入到文件系统中,如果失败,通过abort()方法进行回退。

而获取缓存和缓存的添加过程类似,将url转换为key,然后通过disklrucache的get方法得到一个snapshot对象,接着通过snapshot对象得到缓存的文件输入流。有了文件输入流,bitmap就get到了。

    bitmap bitmap = null;

    string key = hashkeyformurl(url);

    disklrucache.snapshot snapshot = mdisklrucache.get(key);

    if (snapshot != null) {

        fileinputstream fileinputstream = (fileinputstream)snapshot.getinputstream(disk_cache_index);

        filedescriptor filedescriptor = fileinputstream.getfd();

        bitmap = mimageresizer.decodesampledbitmapfromfiledescriptor(filedescriptor,

                reqwidth, reqheight);

        ......

    }

disklrucache优化思考:

disklrucache是基于日志文件的,每次对缓存文件操作都需要进行日志记录,我们可以不用日志文件,在第一次构造disklrucache时,直接从程序访问缓存目录下的文件,并将每个缓存文件的访问时间作为初始值记录在map中的value值,每次访问或保存缓存都更新相应key对应的缓存文件的访问时间,避免了频繁地io操作。

3. 缓存策略对比与总结

  • lrucache是android中已经封装好的类,disklrucache需要导入相应的包才可以使用。
  • 可以在ui线程中直接使用lrucache;使用disklrucache时,由于缓存或者获取都需要对本地文件进行操作,因此要在子线程中实现。
  • lrucache主要用于内存缓存,当app kill掉的时候,缓存也跟着没了;而disklrucache主要用于存储设备缓存,app kill掉的时候,缓存还在。
  • lrucache的内部实现是linkedhashmap,对于元素的添加或获取用put、get方法即可。而disklrucache是通过文件流的形式进行缓存,所以对于元素的添加或获取通过输入输出流来实现。

文章写得比较匆忙,如果有错别字或者理解错的,欢迎和楼主交流~谢谢

来源:https://cloud.tencent.com/developer/article/1013443

Android缓存策略

前言

最近在刷面试题,遇到一个问题,关于缓存的原理的,所以在这里几个笔记,关于缓存很多大牛都说过了,我只是做个笔记,下面的很多都是网上查看到的,并非原创

目录

  • 一:Android 缓存策略
      1. 内存缓存(LruCache)
    • 2.磁盘缓存(文件缓存)——DiskLruCache分析
    • ASimpleCache
  • 二:使用
      1. LRU使用
      1. DiskLruCache
  • 三 源码分析
      1. LRU 源码分析
      1. DiskLruCache分析

一:Android 缓存策略

1. 内存缓存(LruCache)

LRU,全称Least Rencetly Used,即最近最少使用,是一种非常常用的置换算法,也即淘汰最长时间未使用的对象。LRU在操作系统中的页面置换算法中广泛使用,我们的内存或缓存空间是有限的,当新加入一个对象时,造成我们的缓存空间不足了,此时就需要根据某种算法对缓存中原有数据进行淘汰货删除,而LRU选择的是将最长时间未使用的对象进行淘汰。

2. 磁盘缓存(文件缓存)——DiskLruCache分析

JakeWharton/DiskLruCache

不同于LruCache,LruCache是将数据缓存到内存中去,而DiskLruCache是外部缓存,例如可以将网络下载的图片永久的缓存到手机外部存储中去,并可以将缓存数据取出来使用,DiskLruCache不是google官方所写,但是得到了官方推荐

3. ASimpleCache

ASimpleCache如何使用


二:使用

1. LRU使用

(1) 实例化

看下源码

public class LruCache<K, V> {}

键值对的形式

下面是往上很多标准的写法,用于保存图片

    private void init_Lru() {
        //设置LruCache缓存的大小,一般为当前进程可用容量的1/8。
        int maxMemory = (int) (Runtime.getRuntime().totalMemory() / 1024);
        int cacheSize = maxMemory / 8;
        // 重写sizeOf方法,计算出要缓存的每张图片的大小。
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                // 重写sizeOf方法,计算出要缓存的每张图片的大小。
                return value.getRowBytes() * value.getHeight() / 1024;
            }
        };
}

(2)保存

 public final V put(K key, V value) {}

(3)获取

  public final V get(K key) {}

2. DiskLruCache

(1) 权限

因为要操作外部存储,所以必须要先加上权限:

<!-- 在SDCard中创建与删除文件权限 -->  
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>  
<!-- 往SDCard写入数据权限 -->  
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 

另外要从网络下载图片,还要加上权限:

 <uses-permission android:name="android.permission.INTERNET" />

(2)初始化DiskLruCache

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize){}

参数说明

  • File directory 数据的缓存地址
  • int appVersion 当前应用程序的版本号
  • int valueCount 指定同一个key可以对应多少个缓存文件,基本都是传1
  • long maxSize 定最多可以缓存多少字节的数据

获取缓存地址的方法

    /***
     *
     * @param context
     * @param uniqueName 缓存地址的名字
     * @return 缓存地址
     */
    public File getDiskCacheDir(Context context, String uniqueName) {
        String cachePath;
        //当SD卡存在或者SD卡不可被移除
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            // 路径/sdcard/Android/data/<application package>/cache/uniqueName
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            // 路径/data/data/<application package>/cache/uniqueName
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

获取App版本

    /***
     * 
     * @param context
     * @return App 版本
     */
    public int getAppVersion(Context context) {
        try {
            PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
            return info.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 1;
    }

初始化

        File test = getDiskCacheDir(this, "Bitmap");
        int appVersion = getAppVersion(this);
        long maxSize = 10 * 1024 * 1024;
        try {
            mDiskLruCache = DiskLruCache.open(test, appVersion, 1, maxSize);
        } catch (IOException e) {
            e.printStackTrace();
        }

(3) 存数据

String key = hashKeyForDisk(url);  
DiskLruCache.Editor editor = mDiskLruCache.edit(key); 
OuputStream os = editor.newOutputStream(0); 


//进行提交才能使写入生效
 editor.commit();
//表示放弃此次写入
 editor.abort();

生成MD5


    public 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[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }

(4)取数据

 DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);  
if (snapShot != null) {  
    InputStream is = snapShot.getInputStream(0);  
}

(5)删除数据

public synchronized boolean remove(String key) throws IOException

(6)其他API

  • size()
    这个方法会返回当前缓存路径下所有缓存数据的总字节数,以byte为单位,如果应用程序中需要在界面上显示当前缓存数据的总大小,就可以通过调用这个方法计算出来
  • flush()
    这个方法用于将内存中的操作记录同步到日志文件(也就是journal文件)当中。这个方法非常重要,因为DiskLruCache能够正常工作的前提就是要依赖于journal文件中的内容。前面在讲解写入缓存操作的时候我有调用过一次这个方法,但其实并不是每次写入缓存都要调用一次flush()方法的,频繁地调用并不会带来任何好处,只会额外增加同步journal文件的时间。比较标准的做法就是在Activity的onPause()方法中去调用一次flush()方法就可以了
  • close()
    这个方法用于将DiskLruCache关闭掉,是和open()方法对应的一个方法。关闭掉了之后就不能再调用DiskLruCache中任何操作缓存数据的方法,通常只应该在Activity的onDestroy()方法中去调用close()方法。
  • delete()
    这个方法用于将所有的缓存数据全部删除,比如说网易新闻中的那个手动清理缓存功能,其实只需要调用一下DiskLruCache的delete()方法就可以实现了。

三. 源码分析

1. LRU 源码分析

属性

public class LruCache<K, V> {

private int size;// 当前大小
private int maxSize;// 最大容量

private int putCount;// put次数
private int createCount;// 创建次数
private int evictionCount;// 回收次数
private int hitCount;// 命中次数
private int missCount;// 未命中次数
}

构造方法

    /**
     * @param maxSize for caches that do not override {@link #sizeOf}, this is
     *     the maximum number of entries in the cache. For all other caches,
     *     this is the maximum sum of the sizes of the entries in this cache.
     */
    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);
    }
  • 设置了最大容量
  • 可以看到有一个LinkedHashMap,这个是核心,用于储存缓存的对象,
  • 在LinkedHashMap中的第三个参数指定为true,表示这个LinkedHashMap将是基于数据的访问顺序进行排序。为false,则为插入顺序(这里就是核心)

sizeOf && safeSizeOf

 private int safeSizeOf(K key, V value) {
        int result = sizeOf(key, value);
        if (result < 0) {
            throw new IllegalStateException("Negative size: " + key + "=" + value);
        }
        return result;
    }

    /**
     * Returns the size of the entry for {@code key} and {@code value} in
     * user-defined units.  The default implementation returns 1 so that size
     * is the number of entries and max size is the maximum number of entries.
     *
     * <p>An entry's size must not change while it is in the cache.
     */
    protected int sizeOf(K key, V value) {
        return 1;
    }
  • 由于各种数据类型大小测量的标准不统一,具体测量的方法应该由使用者来实现

保存数据

下面代码来自 http://blog.csdn.net/shakespeare001/article/details/51695358

/**
   * 给对应key缓存value,并且将该value移动到链表的尾部。
   */
public final V put(K key, V value) {
    if (key == null || value == null) {
        throw new NullPointerException("key == null || value == null");
    }

      V previous;
      synchronized (this) {
        // 记录 put 的次数
        putCount++;
        // 通过键值对,计算出要保存对象value的大小,并更新当前缓存大小
        size += safeSizeOf(key, value);
        /*
         * 如果 之前存在key,用新的value覆盖原来的数据, 并返回 之前key 的value
         * 记录在 previous
         */
        previous = map.put(key, value);
        // 如果之前存在key,并且之前的value不为null
        if (previous != null) {
            // 计算出 之前value的大小,因为前面size已经加上了新的value数据的大小,此时,需要再次更新size,减去原来value的大小
            size -= safeSizeOf(key, previous);
        }
      }

    // 如果之前存在key,并且之前的value不为null
    if (previous != null) {
        /*
         * previous值被剔除了,此次添加的 value 已经作为key的 新值
         * 告诉 自定义 的 entryRemoved 方法
         */
        entryRemoved(false, key, previous, value);
    }
    //裁剪缓存容量(在当前缓存数据大小超过了总容量maxSize时,才会真正去执行LRU)
    trimToSize(maxSize);
      return previous;
}
  • key和value判空,说明LruCache中不允许key和value为null;
  • 通过safeSizeOf()获取要加入对象数据的大小,并更新当前缓存数据的大小;
  • 将新的对象数据放入到缓存中,即调用LinkedHashMap的put方法,如果原来存在该key时,直接替换掉原来的value值,并返回之前的value值,得到之前value的大小,更新当前缓存数据的size大小;如果原来不存在该key,则直接加入缓存即可;
  • 清理缓存空间,当我们加入一个数据时(put),为了保证当前数据的缓存所占大小没有超过我们指定的总大小,通过调用trimToSize()来对缓存空间进行管理控制。

trimToSize

public void trimToSize(int maxSize) {
    /*
     * 循环进行LRU,直到当前所占容量大小没有超过指定的总容量大小
     */
    while (true) {
        K key;
        V value;
        synchronized (this) {
            // 一些异常情况的处理
            if (size < 0 || (map.isEmpty() && size != 0)) {
                throw new IllegalStateException(
                        getClass().getName() + ".sizeOf() is reporting inconsistent results!");
            }
            // 首先判断当前缓存数据大小是否超过了指定的缓存空间总大小。如果没有超过,即缓存中还可以存入数据,直接跳出循环,清理完毕
            if (size <= maxSize || map.isEmpty()) {
                break;
            }
            /**
             * 执行到这,表示当前缓存数据已超过了总容量,需要执行LRU,即将最近最少使用的数据清除掉,直到数据所占缓存空间没有超标;
             * 根据前面的原理分析,知道,在链表中,链表的头结点是最近最少使用的数据,因此,最先清除掉链表前面的结点
             */
            Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
            key = toEvict.getKey();
            value = toEvict.getValue();
            map.remove(key);
            // 移除掉后,更新当前数据缓存的大小
            size -= safeSizeOf(key, value);
            // 更新移除的结点数量
            evictionCount++;
        }
        /*
         * 通知某个结点被移除,类似于回调
         */
        entryRemoved(true, key, value, null);
    }
}
  • trimToSize()方法的作用就是为了保证当前数据的缓存大小不能超过我们指定的缓存总大小,如果超过了,就会开始移除最近最少使用的数据,直到size符合要求。
  • trimToSize()方法在put()的时候一定会调用,在get()的时候有可能会调用。

public final V get(K key) {}获取数据

/**
 * 根据key查询缓存,如果该key对应的value存在于缓存,直接返回value;
* 访问到这个结点时,LinkHashMap会将它移动到双向循环链表的的尾部。
* 如果如果没有缓存的值,则返回null。(如果开发者重写了create()的话,返回创建的value)
*/
public final V get(K key) {
    if (key == null) {
        throw new NullPointerException("key == null");
    }

    V mapValue;
    synchronized (this) {
        // LinkHashMap 如果设置按照访问顺序的话,这里每次get都会重整数据顺序
        mapValue = map.get(key);
        // 计算 命中次数
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        // 计算 丢失次数
        missCount++;
    }

    /*
     * 官方解释:
     * 尝试创建一个值,这可能需要很长时间,并且Map可能在create()返回的值时有所不同。如果在create()执行的时
     * 候,用这个key执行了put方法,那么此时就发生了冲突,我们在Map中删除这个创建的值,释放被创建的值,保留put进去的值。
     */
    V createdValue = create(key);
    if (createdValue == null) {
        return null;
    }

    /***************************
     * 不覆写create方法走不到下面 *
     ***************************/
    /*
     * 正常情况走不到这里
     * 走到这里的话 说明 实现了自定义的 create(K key) 逻辑
     * 因为默认的 create(K key) 逻辑为null
     */
    synchronized (this) {
        // 记录 create 的次数
        createCount++;
        // 将自定义create创建的值,放入LinkedHashMap中,如果key已经存在,会返回 之前相同key 的值
        mapValue = map.put(key, createdValue);

        // 如果之前存在相同key的value,即有冲突。
        if (mapValue != null) {
            /*
             * 有冲突
             * 所以 撤销 刚才的 操作
             * 将 之前相同key 的值 重新放回去
             */
            map.put(key, mapValue);
        } else {
            // 拿到键值对,计算出在容量中的相对长度,然后加上
            size += safeSizeOf(key, createdValue);
        }
    }

    // 如果上面 判断出了 将要放入的值发生冲突
    if (mapValue != null) {
        /*
         * 刚才create的值被删除了,原来的 之前相同key 的值被重新添加回去了
         * 告诉 自定义 的 entryRemoved 方法
         */
        entryRemoved(false, key, createdValue, mapValue);
        return mapValue;
    } else {
        // 上面 进行了 size += 操作 所以这里要重整长度
        trimToSize(maxSize);
        return createdValue;
    }
}
  • 先尝试从map缓存中获取value,即mapVaule = map.get(key);如果mapVaule != null,说明缓存中存在该对象,直接返回即可;
  • 如果mapVaule == null,说明缓存中不存在该对象,大多数情况下会直接返回null;但是如果我们重写了create()方法,在缓存没有该数据的时候自己去创建一个,则会继续往下走,中间可能会出现冲突,看注释;
  • 注意:在我们通过LinkedHashMap进行get(key)或put(key,value)时都会对链表进行调整,即将刚刚访问get或加入put的结点放入到链表尾部。

entryRemoved()

/**
* 1.当被回收或者删掉时调用。该方法当value被回收释放存储空间时被remove调用
* 或者替换条目值时put调用,默认实现什么都没做。
* 2.该方法没用同步调用,如果其他线程访问缓存时,该方法也会执行。
* 3.evicted=true:如果该条目被删除空间 (表示 进行了trimToSize or remove)  evicted=false:put冲突后 或 get里成功create后
* 导致
* 4.newValue!=null,那么则被put()或get()调用。
*/
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
}
  • 可以发现entryRemoved方法是一个空方法,说明这个也是让开发者自己根据需求去重写的。
  • entryRemoved()主要作用就是在结点数据value需要被删除或回收的时候,给开发者的回调。开发者就可以在这个方法里面实现一些自己的逻辑:
    • 可以进行资源的回收;
    • 可以实现二级内存缓存,可以进一步提高性能,思路如下:

二级缓存

  • 重写LruCache的entryRemoved()函数,
  • 把删除掉的item,再次存入另外一个LinkedHashMap<String, SoftWeakReference<Bitmap>>
  • 这个数据结构当做二级缓存,每次获得图片的时候,先判断LruCache中是否缓存,没有的话,再判断这个二级缓存中是否有,如果都没有再从sdcard上获取。sdcard上也没有的话,就从网络服务器上拉取。

entryRemoved()在LruCache中有四个地方进行了调用:put()、get()、trimToSize()、remove()中进行了调用。


2. DiskLruCache

Android DiskLruCache 源码解析 硬盘缓存的绝佳方案


原文地址

http://blog.csdn.net/shakespeare001/article/details/51695358

https://www.tuicool.com/articles/JB7RNj