if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
//重新加载文件数据
sp.startReloadIfChangedUnexpectedly();
}
//返回SharedPreferences实例对象sp
return sp;
}
我们可以发现,上面方法中首先调运了getSharedPrefsFile来获取一个File对象,所以我们继续先来看下这个方法,具体如下:
public File getSharedPrefsFile(String name) {
//依据我们传入的文件名字符串创建一个后缀为xml的文件
return makeFilename(getPreferencesDir(), name + “.xml”);
}
private File getPreferencesDir() {
synchronized (mSync) {
if (mPreferencesDir == null) {
//获取当前app的data目录下的shared_prefs目录
mPreferencesDir = new File(getDataDirFile(), “shared_prefs”);
}
return mPreferencesDir;
}
}
可以看见,原来SharePreference文件存储路径和文件创建是这个来的。继续往下看可以发现接着调运了SharedPreferencesImpl的构造函数,至于这个构造函数用来干嘛,下面会分析。
好了,到这里我们先回过头稍微总结一下目前的源码分析结论,具体如下:
前面我们有文章分析了Android中的Context,这里又发现ContextImpl中有一个静态的ArrayMap变量sSharedPrefs。这时候你想到了啥呢?无论有多少个ContextImpl对象实例,系统都共享这一个sSharedPrefs的Map,应用启动以后首次使用SharePreference时创建,系统结束时才可能会被垃圾回收器回收,所以如果我们一个App中频繁的使用不同文件名的SharedPreferences很多时这个Map就会很大,也即会占用移动设备宝贵的内存空间,所以说我们应用中应该尽可能少的使用不同文件名的SharedPreferences,取而代之的是合并他们,减小内存使用。同时上面最后一段代码也及具有隐藏含义,其表明了SharedPreferences是可以通过MODE_MULTI_PROCESS来进行夸进程访问文件数据的,其reload就是为了夸进程能更好的刷新访问数据。
好了,还记不记得上面我们分析留的尾巴呢?现在我们就来看看这个尾巴,可以发现SharedPreferencesImpl类其实就是SharedPreferences接口的实现类,其构造函数如下:
final class SharedPreferencesImpl implements SharedPreferences {
…
//构造函数,file是前面分析data目录下创建的传入name的xml文件,mode为传入的访问方式
SharedPreferencesImpl(File file, int mode) {
mFile = file;
//依据文件名创建一个同名的.bak备份文件,当mFile出现crash的会用mBackupFile来替换恢复数据
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
//将文件从flash或者sdcard异步加载到内存中
startLoadFromDisk();
}
…
//创建同名备份文件
private static File makeBackupFile(File prefsFile) {
return new File(prefsFile.getPath() + “.bak”);
}
…
private void startLoadFromDisk() {
//同步操作mLoaded标志,写为未加载,这货是关键的关键!!!!
synchronized (this) {
mLoaded = false;
}
//开启一个线程异步同步加载disk文件到内存
new Thread(“SharedPreferencesImpl-load”) {
public void run() {
synchronized (SharedPreferencesImpl.this) {
//新线程中在SharedPreferencesImpl对象锁中异步load数据,如果此时数据还未load完成,则其他线程调用SharedPreferences.getXXX方法都会被阻塞,具体原因关注mLoaded标志变量即可!!!!!
loadFromDiskLocked();
}
}
}.start();
}
}
好了,到这里你会发现整个SharedPreferencesImpl的构造函数很简单,那我们就继续分析真正的异步加载文件到内存过程,如下:
private void loadFromDiskLocked() {
//如果已经异步加载直接return返回
if (mLoaded) {
return;
}
//如果存在备份文件则直接使用备份文件
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
…
Map map = null;
StructStat stat = null;
try {
//获取Linux文件stat信息,Linux高级C中经常出现的
stat = Os.stat(mFile.getPath());
//文件至少是可读的
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
//把文件以BufferedInputStream流读出来
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
//使用系统提供的XmlUtils工具类将xml流解析转换为map类型数据
map = XmlUtils.readMapXml(str);
} catch (XmlPullParserException e) {
Log.w(TAG, “getSharedPreferences”, e);
} catch (FileNotFoundException e) {
Log.w(TAG, “getSharedPreferences”, e);
} catch (IOException e) {
Log.w(TAG, “getSharedPreferences”, e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
}
//标记置为为已读
mLoaded = true;
if (map != null) {
//把解析的map赋值给mMap
mMap = map;
mStatTimestamp = stat.st_mtime;//记录时间戳
mStatSize = stat.st_size;//记录文件大小
} else {
mMap = new HashMap<String, Object>();
}
//唤醒其他等待线程(其实就是调运该类的getXXX方法的线程),因为在getXXX时会通过mLoaded标记是否进入wait,所以这里需要notify
notifyAll();
}
OK,到此整个Android应用获取SharePreference实例的过程我们就分析完了,简单总结下如下:
创建相关权限和mode的xml文件,异步同步锁加载xml文件并解析xml数据为map类型到内存中等待使用操作,特别注意,在xml文件异步加载未完成时调运SharePreference的getXXX及setXXX方法是阻塞等待的。由此也可以知道,一旦拿到SharePreference对象之后的getXXX操作其实都不再是文件读操作了,也就不存在网上扯蛋的认为多次频繁使用getXXX方法降低性能的说法了。
分析完了构造实例化,我们回忆可以知道使用SharePreference可以通过getXXX方法直接获取已经存在的key-value数据,下面我们就来看下这个过程,这里我们随意看一个方法即可,如下:
public boolean getBoolean(String key, boolean defValue) {
//可以看见,和上面异步load数据使用的是同一个对象锁
synchronized (this) {
//阻塞等待异步加载线程加载完成notify
awaitLoadedLocked();
//加载完成后解析的xml数据放在mMap对象中,我们从mMap中找出指定key的数据
Boolean v = (Boolean)mMap.get(key);
//存在返回找到的值,不存在返回设置的defValue
return v != null ? v : defValue;
}
}
先不解释,我们来关注下上面方法调运的awaitLoadedLocked方法,具体如下:
private void awaitLoadedLocked() {
…
//核心,这就是异步阻塞等待
while (!mLoaded) {
try {
wait();
} catch (InterruptedException unused) {
}
}
}
哈哈,不解释,这也太赤裸裸的明显了,就是阻塞,就是这么任性,没辙。那我们继续攻占高地呗,get完事了,那就是set了呀。
3-3 SharePreferences内部类Editor实现EditorImpl分析
还记不记得set是在SharePreference接口的Editor接口中定义的,而SharePreference提供了edit()方法来获取Editor实例,我们先来看下这个edit()方法吧,如下:
public Editor edit() {
//握草!这也和异步load用的一把锁
synchronized (this) {
//阻塞等待,不解释吧,向上看。。。
awaitLoadedLocked();
}
//异步加载OK以后通过EditorImpl创建Editor实例
return new EditorImpl();
}
可以看见,SharePreference的edit()方法其实就是阻塞等待返回一个Editor的实例(Editor的实现是EditorImpl),那我们就顺藤摸瓜一把,来看下这个EditorImpl这个类,如下:
public final class EditorImpl implements Editor {
//创建一个mModified的key-value集合,用来在内存中暂存数据
private final Map<String, Object> mModified = Maps.newHashMap();
//一个是否清除preference的flag
private boolean mClear = false;
…//省略类似的putXXX方法
public Editor putBoolean(String key, boolean value) {
//同步锁操作
synchronized (this) {
//将我们要存储的数据放入mModified集合中
mModified.put(key, value);
//返回当前对象实例,方便这种模式的代码写法:putXXX().putXXX();
return this;
}
}
//不用过多解释,同步删除mModified中包含key的数据
public Editor remove(String key) {
synchronized (this) {
mModified.put(key, this);
return this;
}
}
//不解释,要清楚所有数据则直接置位mClear标记
public Editor clear() {
synchronized (this) {
mClear = true;
return this;
}
}
…
}
好了,到此你可以发现Editor的setXXX及clear操作仅仅只是将相关数据暂存到内存中或者设置好标记为,也就是说调运了Editor的putXXX后其实数据是没有存入SharePreference的。那么通过我们一开始的实例可以知道,要想将Editor的数据存入SharePreference文件需要调运Editor的commit或者apply方法来生效。所以我们接下来先来看看Editor类常用的commit方法实现原理,如下:
public boolean commit() {
//1.先通过commitToMemory方法提交到内存
MemoryCommitResult mcr = commitToMemory();
//2.写文件操作
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
//阻塞等待写操作完成,UI操作需要注意!!!所以如果不关心返回值可以考虑用apply替代,具体原因等会分析apply就明白了。
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
//3.通知数据发生变化了
notifyListeners(mcr);
//4.返回写文件是否成功状态
return mcr.writeToDiskResult;
}
我去,小小一个commit方法做了这么多操作,主要分为四个步骤,我们先来看下第一个步骤,通过commitToMemory方法提交到内存返回一个MemoryCommitResult对象。分析commitToMemory方法前先看下MemoryCommitResult这个类,具体如下:
// Return value from EditorImpl#commitToMemory()
//也是内部类,只是为了组织数据结构而诞生,也就是EditorImpl.commitToMemory()的返回值
private static class MemoryCommitResult {
public boolean changesMade; // any keys different?
public List keysModified; // may be null
public Set<android.content.SharedPreferences.OnSharedPreferenceChangeListener> listeners; // may be null
public Map<?, ?> mapToWriteToDisk;
public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
public volatile boolean writeToDiskResult = false;
public void setDiskWriteResult(boolean result) {
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}
}
回过头现在来看commitToMemory方法,具体如下:
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
//啥也不说,先整一个实例化对象
MemoryCommitResult mcr = new MemoryCommitResult();
//和SharedPreferencesImpl共用一把锁
synchronized (SharedPreferencesImpl.this) {
// We optimistically don’t make a deep copy until
// a memory commit comes in when we’re already
// writing to disk.
if (mDiskWritesInFlight > 0) {
// We can’t modify our mMap as a currently
// in-flight write owns it. Clone it before
// modifying it.
// noinspection unchecked
//有多个未完成的写操作时复制一份,但是我们不知道用来干啥???????
mMap = new HashMap<String, Object>(mMap);
}
//构造数据结构,把通过SharedPreferencesImpl构造函数里异步加载的文件xml解析结果mMap赋值给要写到disk的Map
mcr.mapToWriteToDisk = mMap;
//增加一个未完成的写opt
mDiskWritesInFlight++;
//判断有没有监听设置
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
//创建监听队列
mcr.keysModified = new ArrayList();
mcr.listeners =
new HashSet<android.content.SharedPreferences.OnSharedPreferenceChangeListener>(mListeners.keySet());
}
//再加一把自己的锁
synchronized (this) {
//如果调运的是Editor的clear方法,则这里commit时这么处理
if (mClear) {
//如果从文件里加载出来的xml不为空
if (!mMap.isEmpty()) {
//设置数据结构中数据变化标志为true
mcr.changesMade = true;
//清空内存中xml数据
mMap.clear();
}
//处理完毕,标记复位,程序继续执行,所以如果这次Editor中如果有写数据且还未commit,则执行完这次commit之后不会清掉本次写操作的数据,只会clear以前xml文件中的所有数据
mClear = false;
}
//mModified是调运Editor的setXXX零时存储的map
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
// “this” is the magic value for a removal mutation. In addition,
// setting a value to “null” for a given key is specified to be
// equivalent to calling remove on that key.
//删除需要删除的key-value
if (v == this || v == null) {
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else {
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
//把变化和新加的数据更新到SharePreferenceImpl的mMap中
mMap.put(k, v);
}
//设置数据结构变化标记
mcr.changesMade = true;
if (hasListeners) {
//设置监听
mcr.keysModified.add(k);
}
}
//清空Editor中零时存储的数据
mModified.clear();
}
}
//返回重新更新过mMap值封装的数据结构
return mcr;
}
到此我们Editor的commit方法的第一步已经完成,根据写操作组织内存数据,返回组织后的数据结构。接下来我们继续回到commit方法看下第二步—-写到文件中,其核心是调运SharedPreferencesImpl类的enqueueDiskWrite方法实现。具体如下:
//按照队列把内存数据写入磁盘,commit时postWriteRunnable为null,apply时不为null
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//创建一个writeToDiskRunnable的Runnable对象
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
//真正的写文件操作
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
//写完一个计数器-1
mDiskWritesInFlight–;
}
if (postWriteRunnable != null) {
//等会apply分析
postWriteRunnable.run();
}
}
};
//判断是同步写还是异步
final boolean isFromSyncCommit = (postWriteRunnable == null);
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
//commit方式走这里
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
//如果当前只有一个写操作
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
//一个写操作就直接在当前线程中写文件,不用另起线程
writeToDiskRunnable.run();
//写完文件就返回
return;
}
}
//如果是apply就在线程池中执行
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
可以发现,commit从内存写文件是在当前调运线程中直接执行的。那我们再来看看这个写内存到磁盘方法中真正的写方法writeToFile,如下:
// Note: must hold mWritingToDiskLock
private void writeToFile(MemoryCommitResult mcr) {
if (mFile.exists()) {
if (!mcr.changesMade) {
//如果文件存在且没有改变的数据则直接返回写OK
mcr.setDiskWriteResult(true);
return;
}
if (!mBackupFile.exists()) {
//如果要写入的文件已经存在,并且备份文件不存在时就先把当前文件备份一份,因为如果本次写操作失败时数据可能已经乱了,所以下次实例化load数据时可以从备份文件中恢复
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn’t rename file " + mFile
- " to backup file " + mBackupFile);
//命名失败直接返回写失败了
mcr.setDiskWriteResult(false);
return;
}
} else {
//备份文件存在就把源文件删掉,因为要写新的
mFile.delete();
}
}
try {
//创建mFile文件
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
//创建失败直接返回写失败了
mcr.setDiskWriteResult(false);
return;
}
//把数据写入mFile文件
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
//彻底同步到磁盘文件中
FileUtils.sync(str);
str.close();
//设置文件权限mode
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
//和刚开始实例化load时一样,更新文件时间戳和大小
try {
final StructStat stat = Os.stat(mFile.getPath());
synchronized (this) {
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
}
} catch (ErrnoException e) {
// Do nothing
}
// Writing was successful, delete the backup file if there is one.
//写成功了mFile那就把备份文件直接删掉,没用了。
mBackupFile.delete();
//设置写成功了,然后返回
mcr.setDiskWriteResult(true);
return;
} catch (XmlPullParserException e) {
Log.w(TAG, “writeToFile: Got exception:”, e);
} catch (IOException e) {
Log.w(TAG, “writeToFile: Got exception:”, e);
}
// Clean up an unsuccessfully written file
if (mFile.exists()) {
//上面如果出错了就删掉,因为写之前已经备份过数据了,下次load时load备份数据
if (!mFile.delete()) {
Log.e(TAG, "Couldn’t clean up partially-written file " + mFile);
}
}
//写失败了
mcr.setDiskWriteResult(false);
}
回过头可以发现,上面commit的第二步写磁盘操作其实是做了类似数据库的事务操作机制的(备份文件)。接着可以继续分析commit方法的第三四步,很明显可以看出,第三步就是回调设置的监听方法,通知数据变化了,第四步就是返回commit写文件是否成功。
总体到这里你可以发现,一个常用的SharePreferences过程已经完全分析完毕。接下来我们就再简单说说Editor的apply方法原理,先来看下Editor的apply方法,如下:
public void apply() {
//有了上面commit分析,这个雷同,写数据到内存,返回数据结构
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
//等待写文件结束
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.add(awaitCommit);
//一个收尾的Runnable
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.remove(awaitCommit);
}
};
//这个上面commit已经分析过的,这里postWriteRunnable不为null,所以会在一个新的线程池调运postWriteRunnable的run方法
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// Okay to notify the listeners before it’s hit disk
// because the listeners should always get the same
// SharedPreferences instance back, which has the
// changes reflected in memory.
//通知变化
notifyListeners(mcr);
}
看到了吧,其实和commit类似,只不过他是异步写的,没在当前线程执行写文件操作,还有就是他不像commit一样返回文件是否写成功状态。
3-4 SharePreferences源码分析总结
题外趣事: 记得好像是去年有一次我用SharePreferences存储了几个boolean值,由于开发调试,当时我直接进入系统data目录下应用的xml存储文件夹,然后执行了删除操作;接着我没有重启应用,直接打断点调试,握草!奇迹的发现SharePreferences调运get时竟然拿到了不是初始化的值。哈哈,其实就是上面分析的,加载完后是一个静态的map,进程没挂之前一直用的内存数据。
通过上面的实例及源码分析可以发现:
-
SharedPreferences在实例化时首先会从sdcard异步读文件,然后缓存在内存中;接下来的读操作都是内存缓存操作而不是文件操作。
-
在SharedPreferences的Editor中如果用commit()方法提交数据,其过程是先把数据更新到内存,然后在当前线程中写文件操作,提交完成返回提交状态;如果用的是apply()方法提交数据,首先也是写到内存,接着在一个新线程中异步写文件,然后没有返回值。
-
由于上面分析了,在写操作commit时有三级锁操作,所以效率很低,所以当我们一次有多个修改写操作时等都批量put完了再一次提交确认,这样可以提高效率。
可以发现,在简单数据行为状态存储中,Android的SharedPreferences是一个安全而且不错的选择。
【工匠若水 http://blog.csdn.net/yanbober 转载请注明出处。点我开始Android技术交流】
4 SharePreferences进阶项目解决方案
其实分析完源码之后也就差不多了。这里所谓的进阶项目解决方案只是曲线救国的2B行为,只是表明还有这么一种方案,至于项目中是否值得提倡那就要综合酌情考虑了。
4-1 SharePreferences存储复杂对象的解决案例
这个案例完全有些多余(因为看完这个例子你会发现还不如使用github上大神流行的ACache更爽呢!),但是也比较有意思,所以还是拿出来说说,其实网上实现的也很多。
我们有时候可能会涉及到存储一个自定义对象到SharedPreferences中,这个怎么实现呢?标准的SharedPreferences的Editor只提供几个常见类型的put方法呀,其实可以实现的,原理就是Base64转码为字符串存储,如下给出我的一个工具类,在项目中可以直接使用:
/**
-
@author 工匠若水
-
@version 1.0
-
http://blog.csdn.net/yanbober
-
保存任意类型对象到SharedPreferences工具类
*/
public final class ObjectSharedPreferences {
private Context mContext;
private String mName;
private int mMode;
public ObjectSharedPreferences(Context context, String name) {
this(context, name, Context.MODE_PRIVATE);
}
public ObjectSharedPreferences(Context context, String name, int mode) {
this.mContext = context;
this.mName = name;
this.mMode = mode;
}
/**
-
保存任意object对象到SharedPreferences
-
@param key
-
@param object
*/
public void setObject(String key, Object object) {
SharedPreferences preferences = mContext.getSharedPreferences(mName, mMode);
ObjectOutputStream objOutputStream = null;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
objOutputStream = new ObjectOutputStream(outputStream);
objOutputStream.writeObject(object);
String objectVal = new String(Base64.encode(outputStream.toByteArray(), Base64.DEFAULT));
SharedPreferences.Editor editor = preferences.edit();
editor.putString(key, objectVal);
editor.commit();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (outputStream != null) {
outputStream.close();
}
if (objOutputStream != null) {
objOutputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
-
从SharedPreferences获取任意object对象
-
@param key
-
@param clazz
-
@return
*/
public T getObject(String key, Class clazz) {
SharedPreferences preferences = this.mContext.getSharedPreferences(mName, mMode);
if (preferences.contains(key)) {
String objectVal = preferences.getString(key, null);
byte[] buffer = Base64.decode(objectVal, Base64.DEFAULT);
ByteArrayInputStream inputStream = new ByteArrayInputStream(buffer);
ObjectInputStream objInputStream = null;
try {
objInputStream = new ObjectInputStream(inputStream);
return (T) objInputStream.readObject();
} catch (StreamCorruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
if (objInputStream != null) {
objInputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
}
最后我想说
为什么很多程序员做不了架构师?
1、良好健康的职业规划很重要,但大多数人都忽略了
2、学习的习惯很重要,持之以恒才是正解。
3、编程思维没能提升一个台阶,局限在了编码,业务,没考虑过选型、扩展
4、身边没有好的架构师引导、培养。所处的圈子对程序员的成长影响巨大。
金九银十面试季,跳槽季,整理面试题已经成了我多年的习惯!在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。
附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
64.DEFAULT);
ByteArrayInputStream inputStream = new ByteArrayInputStream(buffer);
ObjectInputStream objInputStream = null;
try {
objInputStream = new ObjectInputStream(inputStream);
return (T) objInputStream.readObject();
} catch (StreamCorruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
if (objInputStream != null) {
objInputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
}
最后我想说
为什么很多程序员做不了架构师?
1、良好健康的职业规划很重要,但大多数人都忽略了
2、学习的习惯很重要,持之以恒才是正解。
3、编程思维没能提升一个台阶,局限在了编码,业务,没考虑过选型、扩展
4、身边没有好的架构师引导、培养。所处的圈子对程序员的成长影响巨大。
金九银十面试季,跳槽季,整理面试题已经成了我多年的习惯!在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。
附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
[外链图片转存中…(img-3KeDRxfw-1714671341639)]
里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!