int prunedFiles = 0; long startTime = SystemClock.elapsedRealtime();
Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, CacheHeader> entry = iterator.next(); CacheHeader e = entry.getValue(); boolean deleted = getFileForKey(e.key).delete(); if (deleted) { mTotalSize -= e.size; } else { //print log } iterator.remove(); prunedFiles++; if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) { break; } } }
其中mMaxCacheSizeInBytes是构造方法传入的一个缓存文件夹的大小,如果不传默认是5M的大小。
通过这个方法可以发现,每当被调用时会传入一个neededSpace,也就是需要申请的磁盘大小(即要新缓存的那个文件所需大小)。首先会判断如果这个neededSpace申请成功以后是否会超过最大可用容量,如果会超过,则通过遍历本地已经保存的缓存文件的header(header中包含了缓存文件的缓存有效期、占用大小等信息)去删除文件,直到可用容量不大于声明的缓存文件夹的大小。
其中HYSTERESIS_FACTOR是一个值为0.9的常量,应该是为了防止误差的存在吧(我猜的)。
Volley缓存命中率的优化
如果让你去设计Volley的缓存功能,你要如何增大它的命中率。 可惜了,如果上面的缓存功能是昨天看的,今天的面试这个问题就能说出来了。 还是上面的代码,在缓存内容可能超过缓存文件夹的大小时,删除的逻辑是直接遍历header删除。这个时候删除的文件有可能是我们上一次请求时刚刚保存下来的,屁股都还没坐稳呢,现在立即删掉,有点舍不得啊。 如果遍历的时候,判断一下,首先删除超过缓存有效期的(过期缓存),其次按照LRU算法,删除最久未使用的,岂不是更合适?
Volley缓存文件名的计算
这个是我一直没弄懂的问题。 如下代码:
private String getFilenameForKey(String key) { int firstHalfLength = key.length() / 2; String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode()); localFilename += String.valueOf(key.substring(firstHalfLength).hashCode()); return localFilename; }
为什么会要把一个key分成两部分,分别求hashCode,最后又做拼接。
这个问题之前在stackoverflow上问过 #链接
原谅我,别人的回答我最初并没有看懂。直到最近被问到,如果让你设计一个HashMap,如何避免value被覆盖,我才想到原因。
先来看一下 String#hashCode() 的实现:
@Override public int hashCode() { int hash = hashCode; if (hash == 0) { if (count == 0) { return 0; } final int end = count + offset; final char[] chars = value; for (int i = offset; i < end; ++i) { hash = 31*hash + chars[i]; } hashCode = hash; } return hash; }
从上面的实现可以看到,String的hashcode是根据字符数组中每个位置的字母的int值再加上上次hash值乘以31,这种算法求出来的,至于为什么是31,我也不清楚。 但是可以肯定一点,hashcode并不是唯一的。不信你运行下面这两个输出:
System.out.print("======" + "vFrKiaNHfF7t[9::E[XsX?L7xPp3DZSteIZvdRT8CX:w6d;v<KZnhsM^dqoppe".hashCode()); System.out.print("======" + "hI4pFxGOfS@suhVUd:mTo_begImJPB@Fl[6WJ?ai=RXfIx^=Aix@9M;;?Vdj_Zsi".hashCode());
这两个字符串是根据hashcode的算法逆向出来的,他们的hashcode都是12345。逆向算法请见这里 再回到我们的问题,为什么会要把一个key分成两部分。现在可以肯定的答出,目的是为了尽可能避免hashcode重复造成的文件名重复(求两次hash两次都与另一个url重复的概率总要比一次重复的概率小吧)。 顺带再提一点,就像上面说的,概率小并不代表不存在。但是Java计算hashcode的速度是很快的,应该是在效率和安全性上取舍的结果吧。
推送心跳包是TCP包还是UDP包或者HTTP包
其实聊起这个问题是因为最近看到 @睡不着起不来的万宵 同学写的一篇文章《Android推送技术研究》结果就产生了这个没回答出来的问题(妈蛋,自己给自己挖坑 - -)
最后看了这篇文章(好像是转的,没找到原地址)android 心跳的分析
原来心跳包的实现是调用了socket.sendUrgentData(0xFF)这句代码实现的,所以,当然是TCP包。
ARGB_8888占用内存大小
首先说说本题的答案,是4byte,即ARGB各占用8个比特来描述。当时回答错了,详细解答看这里你的 Bitmap 究竟占多大内存 但是—— 这个问题引出了一个大大的闹剧,请听我慢慢道来。😂😂😂 不知道怎么就聊到 Bitmap 压缩上了,他说他们的Bitmap居然都是不压缩的😂😂😂 还是直接甩代码吧。。。。
public static Bitmap create(byte[] bytes, int maxWidth, int maxHeight) { //上面的省略了 option.inJustDecodeBounds = true; BitmapFactory.decodeByteArray(bytes, 0, bytes.length, option); int actualWidth = option.outWidth; int actualHeight = option.outHeight;
// 计算出图片应该显示的宽高 int desiredWidth = getResizedDimension(maxWidth, maxHeight, actualWidth, actualHeight); int desiredHeight = getResizedDimension(maxHeight, maxWidth, actualHeight, actualWidth);
option.inJustDecodeBounds = false; option.inSampleSize = findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight); Bitmap tempBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, option);
// 做缩放 if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth || tempBitmap .getHeight() > desiredHeight)) { bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desiredHeight, true); tempBitmap.recycle(); } else { bitmap = tempBitmap; } }
return bitmap; }
你这么做,decodeByteArray两次不是更占内存吗?😂😂😂 第一次设置inJustDecodeBounds = true 时候是不占内存的,因为返回的是null 一脸不相信我的说:噢,这地方我下去再看看。 吓得我回来了以后赶紧又看了看,还好没有记错,见源码注释
/**
- If set to true, the decoder will return null (no bitmap), but
- the out... fields will still be set, allowing the caller to query
- the bitmap without having to allocate the memory for its pixels. */ public boolean inJustDecodeBounds;
Activity中类似onCreate、onStart运用了哪种设计模式,优点是什么
这个回答的太多了,我当时说的是代理模式,因为AppCompatActivity中的确是使用的代理模式。这一点还要感谢@代码家 当时说让我看看AppCompatDelegate类的设计。其主要目的就是通过使用组合来替代继承,降低了耦合。
不过回家后再想一想,对方想听到的应该是模板方法模式吧。在父类中实现一个算法不变的部分,并将可变的行为留给子类来实现。生命周期方法原本就是在基类中做出了Activity不同状态时回调的一系列方法,而这些方法具体需要做的可变部分交给子类去完成。
HashMap的底层实现
HashMap内部是通过数组实现的,而数组的每一位都是Entry的对象,这个Entry实际上是一个链表的节点。诶,大学时候数据结构有讲过啊,都忘记了。根据hash算法,求出当前key应该存放在数组的那个index处,如果有值了,则存在index所在Entry所在链表相邻的下一个位置。
根据如果自己实现HashMap如何防止value覆盖。同上面 Volley 中讲到的。
Atomic、volatile、synchronized区别
面Java基础的时候遇上了这个问题,说如果只有一个i++;的时候,volatile和synchronized能否互换。当时也不知道,感觉volatile作为修饰变量的时候,变量自加会出现加到一半发生线程调度。再看看当时蒙对了。
volatile 可以保证在一个线程的工作内存中修改了该变量的值,该变量的值立即能回显到主内存中,从而保证所有的线程看到这个变量的值是一致的。但是有个前提,因为它不具有操作的原子性,也就是它不适合在对该变量的写操作依赖于变量本身自己。就比如i++、i+=1;这种。但是可以改为num=i+1;如果i是一个 volatile 类型,那么num就是安全的,总之就是不能作用于自身。
synchronized是基于代码块的,只要包含在synchronized块中,就是线程安全的。
既然都说了线程安全,就多了解几个:
AtomicInteger,一个轻量级的synchronized。使用的并不是同步代码块,而是Lock-Free算法(我也不懂,看代码就是一个死循环调用了底层的比较方法直到相同后才退出循环)。最终的结果就是在高并发的时候,或者说竞争激烈的时候效率比synchronized高一些。
ThreadLocal,线程中私有数据。主要用于线程改变内部的数据时不影响其他线程,使用时需要注意static。
详细分析见这篇文章 。
再补一个,才学到的。利用clone()方法,如果是一个类的多个对象想共用对象内部的一个变量,而又不想这个变量static,可以使用浅复制方式。(查看设计模式原型模式)
其他
做内部库设计时,最重要的考虑是jar的成本,方法数、体积。 设计模式不应该是去记忆,而应该是用的时候自然而然的用上。
3月11日更新 面试真的是有够烦的,因为题目是随机的,而知识是无穷的。直到被很多答案都是没有标准的。就好像上面提到的 MV* ,也许到现在上面的理解依旧有问题,但是我觉得架构是死的,而最合适的才是最好的。 但是有一点,面试也是一种学习,至少它能让你知道你的薄弱点在哪。
由于文章篇幅问题复制链接查看详细文章以及获取学习笔记链接:shimo.im/docs/QVGDhC… 或者可以查看我的【Github】里可以查看
- Android进阶学习全套手册 关于实战,我想每一个做开发的都有话要说,对于小白而言,缺乏实战经验是通病,那么除了在实际工作过程当中,我们如何去更了解实战方面的内容呢?实际上,我们很有必要去看一些实战相关的电子书。目前,我手头上整理到的电子书还算比较全面,HTTP、自定义view、c++、MVP、Android源码设计模式、Android开发艺术探索、Java并发编程的艺术、Android基于Glide的二次封装、Android内存优化——常见内存泄露及优化方案、.Java编程思想 (第4版)等高级技术都囊括其中。
总结
可以看出,笔者的工作学习模式便是由以下 「六个要点」 组成:
❝ 多层次的工作/学习计划 + 番茄工作法 + 定额工作法 + 批处理 + 多任务并行 + 图层工作法❞
希望大家能将这些要点融入自己的工作学习当中,我相信一定会工作与学习地更富有成效。
下面是我学习用到的一些书籍学习导图,以及系统的学习资料。每一个知识点,都有对应的导图,学习的资料,视频,面试题目。
**如:我需要学习 **Flutter的知识。(大家可以参考我的学习方法)
点击这里了解更多即可领取!
- Flutter 的思维导图(无论学习什么,有学习路线都会事半功倍)
- Flutter进阶学习全套手册
- Flutter进阶学习全套视频
大概就上面这几个步骤,这样学习不仅高效,而且能系统的学习新的知识。