全面复盘Android开发者容易忽视的Backup功能 _ 创作者训练营第二期

override fun onFullBackup(data: FullBackupDataOutput?) {
Log.d(Constants.TAG_BACKUP, “onFullBackup()”)
super.onFullBackup(data)
}

override fun onRestoreFile(…
) {
Log.d(Constants.TAG_BACKUP, “onRestoreFile() destination: d e s t i n a t i o n t y p e : destination type: destinationtype:type mode: m o d e m t i m e : mode mtime: modemtime:mtime”)
super.onRestoreFile(data, size, destination, type, mode, mtime)
}

// Callback when restore finished.
override fun onRestoreFinished() {
Log.d(Constants.TAG_BACKUP, “onRestoreFinished()”)
super.onRestoreFinished()
}
}

这样子便可以在定制Backup流程的依然采用自动备份模式,两全其美。

adb backup -f auto-backup.ab -apk com.ellison.backupdemo

adb logcat -s BackupManagerService -s BackupRestoreAgent
BackupRestoreAgent: MyBackupAgent() 
BackupRestoreAgent: onCreate()
BackupManagerService: agentConnected pkg=com.ellison.backupdemo agent=android.os.BinderProxy@3c0bc60
BackupManagerService: got agent android.app.IBackupAgent S t u b Stub StubProxy@4b5a519
BackupManagerService: Calling doFullBackup() on com.ellison.backupdemo
BackupRestoreAgent: onFullBackup() ★
BackupManagerService: Adb backup processing complete.
BackupRestoreAgent: onDestroy()
AndroidRuntime: Shutting down VM
BackupManagerService: Full backup pass complete. ★

注意: 6.0之前的系统尚未支持自动备份模式,allowBackup打开也只支持键值对模式。而fullBackupOnly属性的补充设置也会被系统无视。

ⅴ.进阶定制之限制备份来源

与中国市场上大都售卖无锁版设备不同,海外售卖的不少设备是绑定运营商的。而不同运营商上即便同一个应用,它们预设的数据可能都不同。这时候我们可能需要对备份数据的来源做出限制。

简言之A设备上面备份数据限制恢复到B设备。 在这里插入图片描述

如何实现

因为自动备份模式下不会将数据的appVersionCode传回来,所以判断应用版本的办法行不通。而且有的时候应用版本是一致的,只是运营商不一致。

所以需要我们自己实现,大家可以自行思考。先说我之前想到的几种方案。

  1. 备份的时候将设备的名称埋入SP文件,恢复的时候检查SP文件里的值
  2. 备份的时候将设备的名称埋入新的File文件,恢复的时候检查File文件的值

这俩方案的缺陷: 方案1的缺点在于备份的逻辑会在原有的文件里增加值,会影响现有的逻辑。

方案2增加了新文件,避免对现有的逻辑造成影响,对方案1有所改善。但它和方案1都存在一个潜在的问题。

问题在于无法保证这个新文件首先被恢复到,也就无保证在恢复执行的一开始就知道本次恢复是否需要。

假使恢复进行到了一半,轮到标记新文件的时候才发现本次恢复需要丢弃,那么将会导致数据错乱。因为系统没有提供Roll back已恢复数据的API,如果我们自己也没做好保存和回退旧的文件处理的话,最后必然发生部分文件已恢复部分没恢复的不一致问题。

要理解这个问题就要搞清楚恢复操作针对文件的执行顺序。

自动备份模式在恢复的时候会逐个调用onRestoreFile(),将各个目录下备份的文件回调过来。目录之间的顺序和备份时候的顺序一致,如下备份的代码可以看出来:从根目录的Data开始,接着File目录开始,然后DB和SP文件。

public abstract class BackupAgent extends ContextWrapper {

public void onFullBackup(FullBackupDataOutput data) throws IOException {

// Root dir first.
applyXmlFiltersAndDoFullBackupForDomain(
packageName, FullBackup.ROOT_TREE_TOKEN, manifestIncludeMap,
manifestExcludeSet, traversalExcludeSet, data);
// Data dir next.
traversalExcludeSet.remove(filesDir);
// Database directory.
traversalExcludeSet.remove(databaseDir);
// SharedPrefs.
traversalExcludeSet.remove(sharedPrefsDir);
}
}

文件内的顺序则通过File#list()获取,而这个API是无法保证得到的文件列表都按照abcd的字母排序。所以在File目录下放标记文件不能保证它首先被恢复到。即便放一个a开头的标记文件也不能完全保证。

★推荐方案★

一般的App鲜少在根目录存放数据,而根目录最先被恢复到。所以我推荐的方案是这样的。

备份的时候将设备的名称埋入根目录的特定文件,恢复的时候检查该File文件,在恢复的初期就决定本次恢复是否需要。为了不影响恢复之后的正常使用,最后还要删除这个标记文件。

废话不多说,看下代码。

  • Backup里放入标记文件。

class MyBackupAgent : BackupAgentHelper() {

override fun onFullBackup(data: FullBackupDataOutput?) {
// ★ 在备份执行前先将标记文件写入Data目录
// Make backup source file before full backup invoke.
writeBackupSourceToFile()
super.onFullBackup(data)
}

private fun writeBackupSourceToFile() {
val sourceFile = File(dataDir.absolutePath + File.separator

  • Constants.BACKUP_SOURCE_FILE_PREFIX + Build.MODEL)
    if (!sourceFile.exists()) {
    sourceFile.createNewFile()
    }
    }

    }
  • Restore检查标记文件。

class MyBackupAgent : BackupAgentHelper() {
private var needSkipRestore = false

override fun onRestoreFile(
data: ParcelFileDescriptor?,
size: Long,
destination: File?,
type: Int,
mode: Long,
mtime: Long
) {
if (!needSkipRestore) {
val sourceDevice = readBackupSourceFromFile(destination)
// ★ 备份源设备名和当前名不一致的时候标记需要跳过
// Mark need skip restore if source got and not match current device.
if (!TextUtils.isEmpty(sourceDevice) && !sourceDevice.equals(Build.MODEL)) {
needSkipRestore = true
}
}

if (!needSkipRestore) {
// Invoke restore if skip flag set.
super.onRestoreFile(data, size, destination, type, mode, mtime)
} else {
// ★ 跳过备份但一定要消费stream防止恢复的进程阻塞
// Consume data to keep restore stream go.
consumeData(data!!, size, type, mode, mtime, null)
}
}

private fun readBackupSourceFromFile(file: File?): String {
if (file == null) return “”
var decodeDeviceSource = “”

// Got data file with backup source mark.
if (file.name.startsWith(Constants.BACKUP_SOURCE_FILE_PREFIX)) {
decodeDeviceSource = file.name.replace(Constants.BACKUP_SOURCE_FILE_PREFIX, “”)
}
return decodeDeviceSource
}

@Throws(IOException::class)
fun consumeData(data: ParcelFileDescriptor,
size: Long, type: Int, mode: Long, mtime: Long, outFile: File?) {

}
}

  • 无论是Backup还是Restore都要将标记文件移除。

class MyBackupAgent : BackupAgentHelper() {

override fun onDestroy() {
super.onDestroy()
// 移除标记文件
// Ensure temp source file is removed after backup or restore finished.
ensureBackupSourceFileRemoved()
}

private fun ensureBackupSourceFileRemoved() {
val sourceFile = File(dataDir.absolutePath + File.separator

  • Constants.BACKUP_SOURCE_FILE_PREFIX + Build.MODEL)
    if (sourceFile.exists()) {
    val result = sourceFile.delete()
    }
    }
    }

接下里验证代码能否拦截不同设备的备份文件。先在小米手机里备份文件,然后到Pixel模拟器里恢复这个数据。

  • 小米里备份

adb -s c7a1a50c7d27 backup -f auto-backup-cus-xiaomi.ab -apk com.ellison.backupdemo

adb -s c7a1a50c7d27 logcat -s BackupManagerService -s BackupRestoreAgent
BackupManagerService: — Performing full backup for package com.ellison.backupdemo —
BackupRestoreAgent: onCreate()
BackupManagerService: agentConnected pkg=com.ellison.backupdemo agent=android.os.BinderProxy@5e68506
BackupManagerService: got agent android.app.IBackupAgent S t u b Stub StubProxy@852a7c7
BackupManagerService: Calling doFullBackup() on com.ellison.backupdemo
BackupRestoreAgent: onFullBackup()
// ★标记文件里写入了小米的设备名称并备份了
BackupRestoreAgent: writeBackupSourceToFile() sourceFile:/data/user/0/com.ellison.backupdemo/backup-source-Redmi 6A create:true ★
BackupRestoreAgent: onDestroy()
BackupManagerService: Adb backup processing complete.
BackupRestoreAgent: ensureBackupSourceFileRemoved() sourceFile:/data/user/0/com.ellison.backupdemo/backup-source-Redmi 6A delete:true ★
BackupManagerService: Full backup pass complete.

  • Pixel里恢复,可以看到Pixel的日志里显示跳过了恢复

adb -s emulator-5554 restore auto-backup-cus-xiaomi.ab

adb -s emulator-5554 logcat -s BackupManagerService -s BackupRestoreAgent
BackupManagerService: — Performing full-dataset restore —

BackupRestoreAgent: onRestoreFile() destination:/data/data/com.ellison.backupdemo/backup-source-Redmi 6A type:1 mode:384 mtime:1619355877 currentDevice:sdk_gphone_x86_arm needSkipRestore:false
BackupRestoreAgent: readBackupSourceFromFile() file:/data/data/com.ellison.backupdemo/backup-source-Redmi 6A
BackupRestoreAgent: readBackupSourceFromFile() source:Redmi 6A
BackupRestoreAgent: onRestoreFile() sourceDevice:Redmi 6A
// ★从备份数据里读取到了小米的设备名,不同于Pixel模拟器的名称,设定了跳过恢复的flag
BackupRestoreAgent: onRestoreFile() destination:/data/data/com.ellison.backupdemo/Post.jpg type:1 mode:384 mtime:1619355781 currentDevice:sdk_gphone_x86_arm needSkipRestore:true
BackupRestoreAgent: onRestoreFile() skip restore and consume ★

BackupRestoreAgent: onRestoreFinished()
BackupManagerService: [UserID:0] adb restore processing complete.
BackupRestoreAgent: onDestroy()
BackupManagerService: Full restore pass complete.

Pixel模拟器上重新打开App之后确实没有任何数据。 在这里插入图片描述

当然如果App确实有在根目录下存放数据,那么建议你仍采用这个方案。

只不过需要给这个特定文件加一个a的前缀,以保证它大多数情况下会被先恢复到。当然为了防止极低的概率下它没有首先被恢复,开发者还需自行加上一个Data目录下文件的暂存和回退处理,以防万一。

更高的定制需求

如果发现备份的设备名称不一致的时候,客户的需求并不是丢弃恢复,而是让我们将运营商之间的diff merge进来呢?

这里提供一个思路。在上述方案的基础之上改下就行了。

比如恢复的一开始通过标记的文件发现备份的不一致,丢弃恢复的同时将待恢复的文件都改个别名暂存到本地。应用再次打开的时候读取暂存的数据和当前数据做对比,然后将diff merge进来

ⅵ.BackupAgent和配置规则的混用

BackupAgent和XML配置并不冲突,在backup逻辑里还可以获取配置的设备条件。比如在onFullBackup()里可以利用FullBackupDataOutput的getTransportFlags()来取得相应的Flag来执行相应的逻辑。

  • FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED 对应着设备加密条件
  • FLAG_DEVICE_TO_DEVICE_TRANSFER 对应D2D备份场景条件

class MyBackupAgent: BackupAgentHelper() {

override fun onFullBackup(data: FullBackupDataOutput?) {
Log.d(Constants.TAG_BACKUP, “onFullBackup()”)
super.onFullBackup(data)

if (data != null) {
if ((data.transportFlags and FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED) != 0) {
Log.d(Constants.TAG_BACKUP, “onFullBackup() CLIENT ENCRYPTION NEED”)
}
}
}
}

4.4 键值对备份

键值对备份支持的空间小,而且针对File类型的Backup实现非线程安全,同时需要自行考虑DB这种大空间文件的备份处理,并不推荐使用。

但本着学习的目的还是要了解一下。

ⅰ. 基本定制

使用这个模式需额外指定BackupAgent并实现其细节。

<manifest … >
<application android:allowBackup=“true”
android:backupAgent=“.MyBackupAgent” … >



BackupAgent的实现在于告诉BMS每个类型的文件采用什么Key备份和恢复。可以选择高度定制的复杂办法去实现,当然SDK也提供了简单办法。

  • 复杂办法:直接扩展自BackupAgent抽象类,需要自行实现onBackup()onRestore的细节。包括读取各类型文件并调用对应的Helper实现写入数据到备份文件中以及考虑旧的备份数据的迁移等处理。需要考虑很多细节,代码量很大
  • 简单办法:扩展自系统封装好的BackupAgentHelper类并告知各类型文件对应的KEY和Helper实现即可,高效而简单,但没有提供大容量文件比如DB的备份实现

以扩展BackupAgentHelper的简单办法为例,演示下键值对备份的实现。

  • SP文件的话SDK提供了特定的SharedPreferencesBackupHelper实现
  • File文件对应的Helper实现为FileBackupHelper,只限于file目录的数据
  • 其他类型文件比如Data和DB是没有预设Helper实现的,需要自行实现BackupHelper

// MyBackupAgent.kt
class MyBackupAgent: BackupAgentHelper() {
override fun onCreate() {

// Init helper for data, file, db and sp files.
// Data和DB文件使用FileBackupHelper是无法备份的,此处单纯为了验证下
FileBackupHelper(this, Constants.DATA_NAME).also { addHelper(Constants.BACKUP_KEY_DATA, it) }
FileBackupHelper(this, Constants.DB_NAME).also { addHelper(Constants.BACKUP_KEY_DB, it) }
// File和SP各自使用对应的Helper是可以备份的
FileBackupHelper(this, Constants.FILE_NAME).also { addHelper(Constants.BACKUP_KEY_FILE, it) }
SharedPreferencesBackupHelper(this, Constants.SP_NAME).also { addHelper(Constants.BACKUP_KEY_SP, it) }
}

}

先用bmgr工具执行Backup,然后清除Demo的数据再执行Restore。从日志可以看出来键值对备份和恢复成功进行了。

// 开启bmgr和设置本地传输服务

adb shell bmgr enabled
adb shell bmgr transport com.android.localtransport/.LocalTransport

// Backup

adb shell bmgr backupnow com.ellison.backupdemo
Running incremental backup for 1 requested packages.
Package @pm@ with result: Success
Package com.ellison.backupdemo with result: Success
Backup finished with result: Success

// 清空数据

adb shell pm clear com.ellison.backupdemo

// 查看Backup Token

adb shell dumpsys backup

Ancestral: 0
Current: 1

// Restore

adb shell bmgr restore 01 com.ellison.backupdemo
Scheduling restore: Local disk image
restoreStarting: 1 packages
onUpdate: 0 = com.ellison.backupdemo
restoreFinished: 0
done

Demo的截图显示File和SP备份和恢复成功了。但存放在Data目录的海报和DB目录都失败了。这也验证了上述的结论。 在这里插入图片描述 因为出于备份文件空间的考虑,官方并不建议针对DB文件等大容量文件做键值对备份。理论上可以扩展FileBackupHelper对Data和DB文件做出支持。但Google将关键的备份实现(FileBackupHelperBaseperformBackup_checked())对外隐藏,使得简单扩展变得不可能。

StackOverFlow上针对这个问题有过热烈的讨论,唯一的办法是完全自己实现,但随着自动备份的出现,这个问题似乎已经不再重要

stackoverflow.com/questions/5…

ⅱ.手动发起备份

BackupManager的dataChanged()函数可以告知系统App数据变化了,可以安排备份操作。我们在Demo的Backup Button里添加调用。

class LocalData @Inject constructor(…
val backupManager: BackupManager){
fun backupData() {
backupManager.dataChanged()
}

}

点击这个Backup Button之后等几秒钟,发现Demo的备份任务被安排进Schedule里,意味着备份操作将被系统发起。

adb shell dumpsys backup
Pending key/value backup: 3
BackupRequest{pkg=com.ellison.backupdemo} ★

我们可以强制这个Schedule的执行,也可以等待系统的调度。

adb shell bmgr run

BackupManagerService: clearing pending backups
PFTBT : backupmanager pftbt token=604faa13

BackupManagerService: [UserID:0] awaiting agent for ApplicationInfo{7b6a019 com.ellison.backupdemo}
BackupRestoreAgent: onCreate()
BackupManagerService: [UserID:0] agentConnected pkg=com.ellison.backupdemo agent=android.os.BinderProxy@be4cabf
BackupManagerService: [UserID:0] got agent android.app.IBackupAgent S t u b Stub StubProxy@4eab58c
BackupRestoreAgent: onBackup() ★
BackupRestoreAgent: onDestroy()
BackupManagerService: [UserID:0] Released wakelock:backup-0-1265

在这里插入图片描述

ⅲ.手动发起恢复

除了bmgr工具提供的restore以外还可以通过代码手动触发恢复。但这并不安全会影响应用的数据一致性,所以恢复的API requestRestore()废弃了。

我们来验证下,在Demo的Restore Button里添加BackupManager#requestRestore()的调用。

class LocalData @Inject constructor(…
val backupManager: BackupManager){
fun restoreData() {
backupManager.requestRestore(object: RestoreObserver() {

})
}

}

但点击Button之后等一段时间,恢复的日志没有出现,反倒是弹出了无效的警告。

BackupRestoreApp: LocalData#restoreData()
BackupManager: requestRestore(): Since Android P app can no longer request restoring of its backup.

ⅳ.备份版本不一致的处理

版本不一致意味着恢复之后的逻辑可能会受到影响,这是我们在定制Backup功能时需要着重考虑的问题。

版本不一致的情况有两种。

  1. 现在运行的应用版本比备份时候的版本高,比较常见的场景
  2. 现在运行的应用版本比备份时候的版本低,即App降级,不太常见

默认情况下系统会无视App降级的恢复操作,意味着BackupAgent#onRestore()永远不会被回调。

但如果应用对于旧版本数据的兼容处理比较完善,希望支持降级的情况。那么需要在Manifest里打开restoreAnyVersion 属性,系统将意识到你的兼容并包并回调你的onRestore处理。

无论哪种情况都可以在BackupAgent#onRestore()回调里拿到备份时的版本。然后读取App当前的VersionCode,执行对应的数据迁移或丢弃处理。

class MyBackupAgent: BackupAgentHelper() {

override fun onRestore(
data: BackupDataInput?,
appVersionCode: Int,
newState: ParcelFileDescriptor?
) {
val packageInfo = packageManager.getPackageInfo(packageName, 0)
if (packageInfo.versionCode != appVersionCode) {
// Do something.
// 可以调用BackupDataInput#restoreEntity()
// 或skipEntityData()决定恢复还是丢弃
} else {
super.onRestore(data, appVersionCode, newState)
}
}
}

ⅴ.直接扩展BackupAgent

扩展自BackupAgent的需要考虑诸多细节,对这个方案有兴趣的朋友可以参考BackupAgentHelper的源码,也可以查阅官方说明。

developer.android.google.cn/guide/topic…

4.5 系统App的Backup限制

部分系统App的隐私级别较高,即便手动调用了Backup命令,系统仍将无视。并在日志中给出提示。

BackupManagerService: Beginning adb backup…
BackupManagerService: Starting backup confirmation UI, token=1763174695
BackupManagerService: Waiting for backup completion…
BackupManagerService: acknowledgeAdbBackupOrRestore : token=1763174695 allow=true
BackupManagerService: — Performing adb backup —
BackupManagerService: Package com.android.phone is not eligible for backup, removing.★提示该App不适合备份操作
BackupManagerService: Adb backup processing complete.
BackupManagerService: Full backup pass complete.

这个限制的源码在AppBackupUtils中,解决办法很简单在Manifest文件里明确指定BackupAgent

其实Google的意图很清楚,这些系统级别的App数据要是被窃取将十分危险,默认禁止这个操作。但如果你指定了Backup代理那代表开发者考虑到了备份和恢复的场景,对这个操作进行了默许,备份操作才会被放行。

4.6 实战总结

4.6.1 Backup定制的总结

当我们遇到Backup定制任务的时候认真思考下需求再对症下药。为使得这个流程更加直观,做了个流程图分享给大家。 在这里插入图片描述

4.6.2 Backup相关属性
相关属性说明
allowBackup是否支持Backup,默认为true
backupAgent指定Backup代理进行定制
fullBackupContent指定备份规则XML文件
restoreAnyVersion是否支持高版本数据恢复到低版本应用,默认为false
fullBackupOnly在指定了BackupAgent后仍然采用AutoBackup模式
killAfterRestore全系统恢复期后是否终止应用,默认为 true
backupInForeground即使应用处于前台也可以对其执行自动备份,默认为false
clientSideEncryption只在手机设置密钥的情况下执行备份
deviceToDeviceTransfer只在D2D设备间备份的情况下执行备份

5. Android 12的影响和Backup功能的发展历程

Android 12 Beta版即将公开,其针对Backup功能又做了些改动,先来看看变更的说明。

5.1 D2D 设备到设备备份的规则细分

For apps running on and targeting Android 12 and higher:

  • Specifying android:allowBackup="false" does disable backups to Google Drive, but doesn’t disable D2D transfers for the app.
  • Specifying include and exclude rules with the XML configuration mechanism no longer affects D2D transfers, though it still affects Google Drive backups. To specify rules for D2D transfers, you must use the new configuration covered in the next section.

简直之,Android 12开始即便关闭了allowBackup属性,D2D的Backup功能仍将有效,不再受影响。同时原有的通过fullBackupContent指定的配置规则也将失效。

如果你的App目标版本是Android 12的话,需要使用新属性dataExtractionRules来指定语法规则。

语法规则的所变化主要体现在使用新的属性cloud-backupdevice-transfer明示地区分云端备份和D2D备份的规则,而不再像之前那样采用full-backup-content指定统一的规则。

另外原有的设备条件flag也发生了变化。

  • clientSideEncryption:在新规则里变成了disableIfNoEncryptionCapabilities,且只能应用在cloud-backup标签内
  • deviceToDeviceTransfer:新规则将D2D区分开来了,所以这个flag不需要了

<application
android:dataExtractionRules=“new_config.xml”
…>

原因在于云端备份存在空间的限制,难免需要对备份的文件做出取舍。而D2D的场景文件是存在本地的,没有这种限制了却还对备份文件做出削减显然不太合理。

具体细节可参考官方文档。

developer.android.google.cn/about/versi…

5.2 adb backup命令的限制

To help protect private app data, Android 12 changes the default behavior of the adb backup command. For apps that target Android 12, when a user runs the adb backup command, app data is excluded from any other system data that is exported from the device.

adb backup命令是可以备份整机数据的,从Android 12开始该数据里将不包含App部分的应用数据。除非在Manifest里手动打开debuggable属性。

如果备份单个App也失败的话,那安全性将大大提高。笔者在12 Preview版本上执行该命令仍旧能够正常备份。不知道是不是Target SDK的问题,等正式版出来后再尝试下。

详情可参考官方说明。

developer.android.google.cn/about/versi…

5.3 Backup功能的发展历程

简要回顾下Backup功能的发展历程,供快速查阅。

版本变化内容
Android 1.6加入allowBackup属性默认关闭
Android 2.2开始使用键值对备份模式
Android 6.0开始支持自动备份模式,默认打开allowBackup属性
Android 7.0Backup功能将自动备份和恢复用户授予App的权限
Android 9.0新增了加密存储备份文件
Android 12D2D场景的Backup规则变更和adb backup命令的限制

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
img

如何做好面试突击,规划学习方向?

面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。

学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。

同时我还搜集整理2020年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节

image

在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。

image

点击:《Android架构视频+BAT面试专题PDF+学习笔记》即可免费获取~

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。

学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。

同时我还搜集整理2020年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节

[外链图片转存中…(img-nGkHAHy7-1710686624567)]

在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。

[外链图片转存中…(img-J18exEHb-1710686624567)]

点击:《Android架构视频+BAT面试专题PDF+学习笔记》即可免费获取~

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值