Android之备份服务(Bacackupanagerervice)
代码路径
注:Android 7.1源码frameworks/base/core/java/android/app/backupframeworks/base/core/java/com/android/internal/backupframeworks/base/services/backup
启动
com.android.server.SystemServer private static final String BACKUP_MANAGER_SERVICE_CLASS = "com.android.server.backup.BackupManagerService$Lifecycle"; private void startOtherServices() { ··· if (!disableNonCoreServices) { if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_BACKUP)) {//这个是软功能,通过system/etc下面的xml文件控制 mSystemServiceManager.startService(BACKUP_MANAGER_SERVICE_CLASS); } ···· } ··· } com.android.server.backup.BackupManagerService static Trampoline sInstance; static Trampoline getInstance() { // Always constructed during system bringup, so no need to lazy-init return sInstance; } public static final class Lifecycle extends SystemService { public Lifecycle(Context context) { super(context); sInstance = new Trampoline(context); } @Override public void onStart() { publishBinderService(Context.BACKUP_SERVICE, sInstance);//将BACKUP_SERVICE注册到ServiceManager,注意sInstance是Trampoline对象 } @Override public void onUnlockUser(int userId) { if (userId == UserHandle.USER_SYSTEM) { sInstance.initialize(userId);//初始化状态,这里涉及BackupManagerService的初始化 ···· } } ··· }com.android.server.backup.Trampoline1)Trampoline extends IBackupManager.Stub2) volatile BackupManagerService mService; public Trampoline(Context context) { mContext = context; File dir = new File(Environment.getDataDirectory(), "backup"); dir.mkdirs(); mSuppressFile = new File(dir, BACKUP_SUPPRESS_FILENAME); mGlobalDisable = SystemProperties.getBoolean(BACKUP_DISABLE_PROPERTY, false); } // internal control API public void initialize(final int whichUser) { // Note that only the owner user is currently involved in backup/restore // TODO: http://b/22388012 if (whichUser == UserHandle.USER_SYSTEM) { // Does this product support backup/restore at all? if (mGlobalDisable) {//通过此值可以判断是否初始化成功 Slog.i(TAG, "Backup/restore not supported"); return; } synchronized (this) { if (!mSuppressFile.exists()) {//mSuppressFile 也会抑制mService初始化 mService = new BackupManagerService(mContext, this); } else { Slog.i(TAG, "Backup inactive in user " + whichUser); } } } } 小结控制BackupManagerService启动的因素有几种:a.config.disable_noncore 是否正常 默认falseb.android.software.backup 是否在/system/etc/**.xml定义c.a和b正常,则可通过命令检查:adb shell dumpsys backup 是否有Inactived.如果是不激活状态,可查具体原因:a)adb shell getprop ro.backup.disable true代表不激活服务b)/data/backup/backup-suppress 文件存在代表不激活服务
接口分析 fullBackup -- 全量备份接口
案例
IBackupManager bm = IBackupManager.Stub.asInterface( ServiceManager.getService(Context.BACKUP_SERVICE));if (bm != null) { ParcelFileDescriptor pf = ParcelFileDescriptor.open(mAppBackupFile, ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_TRUNCATE); bm.fullBackup(pf, true, false, false, false, false, true, false, new String[]{mCurrentUpgradeItem.getName()});}问题:接口具体备份了哪些文件?怎么实现的?
a)简介
/** * Write a full backup of the given package to the supplied file descriptor. * The fd may be a socket or other non-seekable destination. If no package names * are supplied, then every application on the device will be backed up to the output. * * This method is synchronous -- it does not return until the backup has * completed. * *
Callers must hold the android.permission.BACKUP permission to use this method. * * @param fd The file descriptor to which a 'tar' file stream is to be written * @param includeApks If true
, the resulting tar stream will include the * application .apk files themselves as well as their data. * @param includeObbs If true
, the resulting tar stream will include any * application expansion (OBB) files themselves belonging to each application. * @param includeShared If true
, the resulting tar stream will include * the contents of the device's shared storage (SD card or equivalent). * @param allApps If true
, the resulting tar stream will include all * installed applications' data, not just those named in the packageNames
* parameter. * @param allIncludesSystem If {@code true}, then {@code allApps} will be interpreted * as including packages pre-installed as part of the system. If {@code false}, * then setting {@code allApps} to {@code true} will mean only that all 3rd-party * applications will be included in the dataset. * @param packageNames The package names of the apps whose data (and optionally .apk files) * are to be backed up. The allApps
parameter supersedes this. */ void fullBackup(in ParcelFileDescriptor fd, boolean includeApks, boolean includeObbs, boolean includeShared, boolean doWidgets, boolean allApps, boolean allIncludesSystem, boolean doCompress, in String[] packageNames);
b)具体流程
1)Trampoline.fullBackup -- > BackupManagerService.fullBackup
public void fullBackup(ParcelFileDescriptor fd, boolean includeApks, boolean includeObbs, boolean includeShared, boolean doWidgets, boolean doAllApps, boolean includeSystem, boolean compress, String[] pkgList) { ······ try { ······ FullBackupParams params = new FullBackupParams(fd, includeApks, includeObbs, includeShared, doWidgets, doAllApps, includeSystem, compress, pkgList); final int token = generateToken(); synchronized (mFullConfirmations) { mFullConfirmations.put(token, params);//高级用法,将自定义变量放在本地,只是把标签跨进程传出去 } ······ // start up the confirmation UI if (!startConfirmationUi(token, FullBackup.FULL_BACKUP_INTENT_ACTION)) {//增加用户体验:调用UI进程让用户自行选择是否需要备份 mFullConfirmations.delete(token); return; } ······· // start the confirmation countdown startConfirmationTimeout(token, params);//监听UI进程的反馈 // wait for the backup to be performed waitForCompletion(params);//等待 } finally { ······ } } boolean startConfirmationUi(int token, String action) {//注意:这里可以定制化 try { Intent confIntent = new Intent(action); confIntent.setClassName("com.android.backupconfirm", "com.android.backupconfirm.BackupRestoreConfirmation"); confIntent.putExtra(FullBackup.CONF_TOKEN_INTENT_EXTRA, token);//注意只传了一个token confIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mContext.startActivityAsUser(confIntent, UserHandle.SYSTEM); } catch (ActivityNotFoundException e) { return false; } return true; } 关注进程间通信: A-->System_server System_server-->B B-->System_server 此过程在没有回调方法的前提下,怎么实现数据对接的?又是怎么监听跨进程的任务完成?
2)BackupRestoreConfirmation.sendAcknowledgement
代码路径:frameworks/base/packages/BackupRestoreConfirmation/src/com/android/backupconfirm/BackupRestoreConfirmation.javapublic class BackupRestoreConfirmation extends Activity { @Override public void onCreate(Bundle icicle) { ······ mAllowButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendAcknowledgement(mToken, true, mObserver); ······ } }); ······ } void sendAcknowledgement(int token, boolean allow, IFullBackupRestoreObserver observer) { try { mBackupManager.acknowledgeFullBackupOrRestore(mToken, allow, String.valueOf(mCurPassword.getText()), String.valueOf(encPassword), mObserver);//UI进程调进system_server进程,关注mToken } catch (RemoteException e) { } }}
3)Trampoline.acknowledgeFullBackupOrRestore -- > BackupManagerService.acknowledgeFullBackupOrRestore
public void acknowledgeFullBackupOrRestore(int token, boolean allow, String curPassword, String encPpassword, IFullBackupRestoreObserver observer) { ······ try { FullParams params; synchronized (mFullConfirmations) { params = mFullConfirmations.get(token);//通过token获取保存的对象 if (params != null) { mBackupHandler.removeMessages(MSG_FULL_CONFIRMATION_TIMEOUT, params);//移除超时监听 mFullConfirmations.delete(token);//从队列中删除保存对象 if (allow) { final int verb = params instanceof FullBackupParams ? MSG_RUN_ADB_BACKUP//因为params为FullBackupParams,所以是MSG_RUN_ADB_BACKUP : MSG_RUN_ADB_RESTORE; params.observer = observer;//跨进程的回调对象 ······ Message msg = mBackupHandler.obtainMessage(verb, params); mBackupHandler.sendMessage(msg); } else { ······ } } else { Slog.w(TAG, "Attempted to ack full backup/restore with invalid token"); } } } finally { ······ } }
4)BackupManagerService.BackupHandler(what=MSG_RUN_ADB_BACKUP)
BackupHandler public void handleMessage(Message msg) case MSG_RUN_ADB_BACKUP: { FullBackupParams params = (FullBackupParams)msg.obj; PerformAdbBackupTask task = new PerformAdbBackupTask(params.fd, params.observer, params.includeApks, params.includeObbs, params.includeShared, params.doWidgets, params.curPassword, params.encryptPassword, params.allApps, params.includeSystem, params.doCompress, params.packages, params.latch); (new Thread(task, "adb-backup")).start();//执行线程,关注task break; }5)BackupManagerService.PerformAdbBackupTask.runPerformAdbBackupTask public void run() { ······ sendStartBackup();//通知回调方法onStartBackup ······ if (mPackages != null) { addPackagesToSet(packagesToBackup, mPackages);//packagesToBackup初始化需要备份的app } ······ ArrayList backupQueue = new ArrayList(packagesToBackup.values());//需要备份的队列 FileOutputStream ofstream = new FileOutputStream(mOutputFile.getFileDescriptor());//mOutputFile就是fullBackup传入的fd OutputStream out = null; ······ try { ······ OutputStream finalOutput = ofstream; ······ try { ······ out = finalOutput; } catch (Exception e) { ······ } ······ int N = backupQueue.size(); for (int i = 0; i < N; i++) { pkg = backupQueue.get(i); final boolean isSharedStorage = pkg.packageName.equals(SHARED_BACKUP_AGENT_PACKAGE); mBackupEngine = new FullBackupEngine(out, null, pkg, mIncludeApks, this); sendOnBackupPackage(isSharedStorage ? "Shared storage" : pkg.packageName);//通知回调方法onBackupPackage mBackupEngine.backupOnePackage();//执行app的备份工作 ······ } } catch (RemoteException e) { } catch (Exception e) { } finally { ······ synchronized (mLatch) { mLatch.set(true); mLatch.notifyAll();//这里就是解除fullBackup的等待 } sendEndBackup();//通知回调方法onEndBackup ······· } }
5)BackupManagerService.FullBackupEngine.backupOnePackage
FullBackupEngine public int backupOnePackage() throws RemoteException { int result = BackupTransport.AGENT_ERROR; if (initializeAgent()) {//初始化需要备份的app的mAgent。后面专题特殊说明 ParcelFileDescriptor[] pipes = null; try { pipes = ParcelFileDescriptor.createPipe();//创建管道,[0]读 [1]写 ······ final int token = generateToken(); FullBackupRunner runner = new FullBackupRunner(mPkg, mAgent, pipes[1], token, sendApk, !isSharedStorage, widgetBlob); pipes[1].close(); // the runner has dup'd it pipes[1] = null; Thread t = new Thread(runner, "app-data-runner"); t.start();//将数据写入管道[1]中,这也是一种跨进程的通信。解决跨进程间获取资源文件问题 routeSocketDataToOutput(pipes[0], mOutput);//从管道[0]中读出来的数据,写入我们fullBackup制定的fd中 ······ } catch (IOException e) { } finally { } } else { Slog.w(TAG, "Unable to bind to full agent for " + mPkg.packageName); } ······ return result; }高级用法:采用匿名管道实现跨进程获取资源文件
6)BackupManagerService.FullBackupEngine.FullBackupRunner.run
FullBackupRunner public void run() { try { FullBackupDataOutput output = new FullBackupDataOutput(mPipe); ······· if (mSendApk) { writeApkToBackup(mPackage, output);//备份apk } ······ mAgent.doFullBackup(mPipe, mToken, mBackupManagerBinder);//备份apk的用户数据 } catch (IOException e) { } catch (RemoteException e) { } finally { } } private void writeApkToBackup(PackageInfo pkg, FullBackupDataOutput output) { ······ final String appSourceDir = pkg.applicationInfo.getBaseCodePath();//路径,例如/data/app/com.test.test-1/base.apk final String apkDir = new File(appSourceDir).getParent(); FullBackup.backupToTar(pkg.packageName, FullBackup.APK_TREE_TOKEN, null, apkDir, appSourceDir, output);//打包apk ······ } 注意:1.output仅仅是管道[1]的载体,真正写到我们指定的文件是通过从管道[0]中读出数据,填充到指定fd2.备份的apk文件路径为:/data/app/***/base.apk3.调用android.app.backup.FullBackup.backupToTar方法
7)BackupManagerService.FullBackupEngine.mAgent.doFullBackup(mPipe, mToken, mBackupManagerBinder); 注: 此业务代码路线比较长,需要特别拿出来说
(a)先说重点,此方法打包的就是app运行的用户资源。例如:
/data/user/0/包名/*
(b)代码实现
(1)mAgent初始化:BackupManagerService.FullBackupEngine.initializeAgent
BackupManagerService.FullBackupEngine.backupOnePackage可知: BackupManagerService.FullBackupEngine.initializeAgent private boolean initializeAgent() { if (mAgent == null) { mAgent = bindToAgentSynchronous(mPkg.applicationInfo, IApplicationThread.BACKUP_MODE_FULL); } return mAgent != null; }
(2)BackupManagerService.bindToAgentSynchronous
IBackupAgent bindToAgentSynchronous(ApplicationInfo app, int mode) { IBackupAgent agent = null; synchronized(mAgentConnectLock) { mConnecting = true; mConnectedAgent = null; try { if (mActivityManager.bindBackupAgent(app.packageName, mode, UserHandle.USER_OWNER)) {//通过Ams绑定app获取mConnectedAgent long timeoutMark = System.currentTimeMillis() + TIMEOUT_INTERVAL; while (mConnecting && mConnectedAgent == null && (System.currentTimeMillis() < timeoutMark)) {//等待mConnectedAgent被赋值 try { mAgentConnectLock.wait(5000);//休眠等待被通知 } catch (InterruptedException e) { // just bail Slog.w(TAG, "Interrupted: " + e); mConnecting = false; mConnectedAgent = null; } } ······ agent = mConnectedAgent;//期待在等待的时间内,mConnectedAgent被赋予成功 } } catch (RemoteException e) { } } if (agent == null) { try { mActivityManager.clearPendingBackup(); } catch (RemoteException e) { // can't happen - ActivityManager is local } } return agent; }
(3)ActivityManagerService.bindBackupAgent
public boolean bindBackupAgent(String packageName, int backupMode, int userId) { ······ synchronized(this) { ······ ProcessRecord proc = startProcessLocked(app.processName, app, false, 0, "backup", hostingName, false, false, false);//启动app if (proc == null) { Slog.e(TAG, "Unable to start backup agent process " + r); return false; } ······ if (proc.thread != null) { ······ try { proc.thread.scheduleCreateBackupAgent(app, compatibilityInfoForPackageLocked(app), backupMode);//执行IApplicationThread,scheduleCreateBackupAgent } catch (RemoteException e) { // Will time out on the backup manager side } } else { } ······ } return true; }
(4)ApplicationThreadNative.java ApplicationThreadProxy.scheduleCreateBackupAgent
代码路径:frameworks/base/core/java/android/app/ApplicatinThreadNativeApplicationThreadProxy public final void scheduleCreateBackupAgent(ApplicationInfo app, CompatibilityInfo compatInfo, int backupMode) throws RemoteException { Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); app.writeToParcel(data, 0); compatInfo.writeToParcel(data, 0); data.writeInt(backupMode); mRemote.transact(SCHEDULE_CREATE_BACKUP_AGENT_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); data.recycle(); }ApplicationThreadNative public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { ······ case SCHEDULE_CREATE_BACKUP_AGENT_TRANSACTION: { data.enforceInterface(IApplicationThread.descriptor); ApplicationInfo appInfo = ApplicationInfo.CREATOR.createFromParcel(data); CompatibilityInfo compatInfo = CompatibilityInfo.CREATOR.createFromParcel(data); int backupMode = data.readInt(); scheduleCreateBackupAgent(appInfo, compatInfo, backupMode);//执行ActivityThread.scheduleCreateBackupAgent return true; } }
(5)ActivityThread.scheduleCreateBackupAgent
代码路径:frameworks/base/core/java/android/app/ActivityThread public final void scheduleCreateBackupAgent(ApplicationInfo app, CompatibilityInfo compatInfo, int backupMode) { CreateBackupAgentData d = new CreateBackupAgentData(); d.appInfo = app; d.compatInfo = compatInfo; d.backupMode = backupMode; sendMessage(H.CREATE_BACKUP_AGENT, d); } ActivityThread.H.handleMessage(what=CREATE_BACKUP_AGENT) --> ActivityThread.handleCreateBackupAgent private void handleCreateBackupAgent(CreateBackupAgentData data) { ······ String classname = data.appInfo.backupAgentName; // full backup operation but no app-supplied agent? use the default implementation if (classname == null && (data.backupMode == IApplicationThread.BACKUP_MODE_FULL || data.backupMode == IApplicationThread.BACKUP_MODE_RESTORE_FULL)) { classname = "android.app.backup.FullBackupAgent";//默认Agent类 } try { IBinder binder = null; BackupAgent agent = mBackupAgents.get(packageName); if (agent != null) { ······ binder = agent.onBind(); } else { try { java.lang.ClassLoader cl = packageInfo.getClassLoader(); agent = (BackupAgent) cl.loadClass(classname).newInstance(); // set up the agent's context ContextImpl context = ContextImpl.createAppContext(this, packageInfo); context.setOuterContext(agent); agent.attach(context); agent.onCreate(); binder = agent.onBind();//获取agent的binder对象 mBackupAgents.put(packageName, agent); } catch (Exception e) { } } // tell the OS that we're live now try { ActivityManagerNative.getDefault().backupAgentCreated(packageName, binder);//调用AMS.backupAgentCreated } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } catch (Exception e) { throw new RuntimeException("Unable to create BackupAgent " + classname + ": " + e.toString(), e); } } 跨进程调入app内部,再从app内部跨进程告知Ams,并带上binder对象
(6)ActivityManagerService.backupAgentCreated
public void backupAgentCreated(String agentPackageName, IBinder agent) { ······ try { IBackupManager bm = IBackupManager.Stub.asInterface( ServiceManager.getService(Context.BACKUP_SERVICE)); bm.agentConnected(agentPackageName, agent);//调进BackupManagerService.agentConnected } catch (RemoteException e) { } catch (Exception e) { } finally { } }
(7)Trampoline.agentConnected-->BackupManagerService.agentConnected
public void agentConnected(String packageName, IBinder agentBinder) { synchronized(mAgentConnectLock) { if (Binder.getCallingUid() == Process.SYSTEM_UID) { IBackupAgent agent = IBackupAgent.Stub.asInterface(agentBinder); mConnectedAgent = agent;//终于见到本尊的初始化了 mConnecting = false; } else { Slog.w(TAG, "Non-system process uid=" + Binder.getCallingUid() + " claiming agent connected"); } mAgentConnectLock.notifyAll();//唤醒对应的线程,可见(2) } } 至此,mConnectedAgent的对象已知,为BackupAgent
(c)BackupAgent.BackupServiceBinder.doFullBackup
BackupAgent.BackupServiceBinder.doFullBackup public void doFullBackup(ParcelFileDescriptor data, int token, IBackupManager callbackBinder) { ······ try { BackupAgent.this.onFullBackup(new FullBackupDataOutput(data)); } catch (IOException ex) { } catch (RuntimeException ex) { } finally { } } BackupAgent.onFullBackup public void onFullBackup(FullBackupDataOutput data) throws IOException { //这里涉及app的用户数据备份,例如:sp、db //最关键的applyXmlFiltersAndDoFullBackupForDomain //fullBackupFileTree-->FullBackup.backupToTar }
8)重点解读:FullBackup.backupToTar
代码路径:frameworks/base/core/java/android/app/backup/FullBackup.javaframeworks/base/core/jni/android_app_backup_FullBackup.cppframeworks/base/libs/androidfw/BackupHelpers.cppFullBackup.javastatic public native int backupToTar(String packageName, String domain, String linkdomain, String rootpath, String path, FullBackupDataOutput output);android_app_backup_FullBackup.cppstatic jint backupToTar(JNIEnv* env, jobject clazz, jstring packageNameObj, jstring domainObj, jstring linkdomain, jstring rootpathObj, jstring pathObj, jobject dataOutputObj) { ······ jint err = write_tarfile(packageName, domain, rootpath, path, &tarSize, writer); if (!err) { //ALOGI("measured [%s] at %lld", path.string(), (long long) tarSize); env->CallVoidMethod(dataOutputObj, sFullBackupDataOutput.addSize, (jlong) tarSize); } return err;}BackupHelpers.cppint write_tarfile(const String8& packageName, const String8& domain, const String8& rootpath, const String8& filepath, off_t* outSize, BackupDataWriter* writer){ //可以自行查看代码 ········}
c)简化流程
1)备份业务:BackupManagerService.fullBackup|||/BackupRestoreConfirmation(调用BackupManagerService.acknowledgeFullBackupOrRestore区分点FULL_BACKUP_INTENT_ACTION)|||/BackupManagerService MSG_RUN_ADB_BACKUP (BackupHandler) PerformAdbBackupTask (FullBackupParams) run 调用如下1-BackupManagerService|||/1-BackupManagerService FullBackupEngine FullBackupRunner writeApkToBackup//备份app mAgent.doFullBackup(mPipe, mToken, mBackupManagerBinder);//备份app涉及的数据关键点:FullBackup.backupToTar2)恢复业务:BackupManagerService.fullRestore|||/BackupRestoreConfirmation(调用BackupManagerService.acknowledgeFullBackupOrRestore区分点FULL_RESTORE_INTENT_ACTION)|||/BackupManagerService MSG_RUN_ADB_RESTORE (BackupHandler) PerformAdbRestoreTask (FullRestoreParams) run restoreOneFile installApk3)访问的权限要求android.permission.BACKUP --- signature|privileged
亮点解读
1.怎么实现跨进程等待?
案例1): 调用接口fullBackup,需要等待用户确认
等待过程: public final AtomicBoolean latch; void waitForCompletion(FullParams params) { synchronized (params.latch) { while (params.latch.get() == false) { try { params.latch.wait(); } catch (InterruptedException e) { /* never interrupted */ } } } }确认过程(确认后,等待的wait自动退出)方案1) void signalFullBackupRestoreCompletion(FullParams params) { synchronized (params.latch) { params.latch.set(true); params.latch.notifyAll(); } } 方案2) final AtomicBoolean mLatch; synchronized (mLatch) { mLatch.set(true); mLatch.notifyAll(); }总结:对象的wait和notifyAll方法使用
案例2):
备份app数据需要启动Agent,等待确认
等待过程: IBackupAgent bindToAgentSynchronous(ApplicationInfo app, int mode) { synchronized(mAgentConnectLock) { try { if (mActivityManager.bindBackupAgent(app.packageName, mode, UserHandle.USER_OWNER)) { long timeoutMark = System.currentTimeMillis() + TIMEOUT_INTERVAL; while (mConnecting && mConnectedAgent == null && (System.currentTimeMillis() < timeoutMark)) { try { mAgentConnectLock.wait(5000); } catch (InterruptedException e) { } } } } catch (RemoteException e) { // can't happen - ActivityManager is local } } } 确认过程 public void agentConnected(String packageName, IBinder agentBinder) { synchronized(mAgentConnectLock) { ····· mAgentConnectLock.notifyAll(); } } 总结对象的wait和notifyAll方法使用
2.app的用户数据怎么被打包到指定文件?
采用管道,因为父子进程(fork)共享内存拷贝ParcelFileDescriptor[] pipes = ParcelFileDescriptor.createPipe();[0] 读[1] 写参考学习:https://www.jianshu.com/p/c2a8987e1c0dhttps://www.jianshu.com/p/115cf0e519c2
问题记录:
aAsset path /data/cache/backup_stage/**.apk is neither a directory nor file (type=0).
参考学习
https://www.52pojie.cn/forum.php?mod=viewthread&tid=1060641https://github.com/IzZzI/BackUpDemohttps://bbs.125.la/thread-14425656-1-1.html?goto=lastposthttps://blog.csdn.net/self_study/article/details/58587412https://blog.csdn.net/u013334392/article/details/81392097https://www.jianshu.com/p/c2a8987e1c0dhttps://www.jianshu.com/p/115cf0e519c2http://www.bubuko.com/infodetail-1890598.htmlhttps://www.cnblogs.com/lipeineng/p/6237681.html
更多相关文章
- OpenGL,Android注意事项初始化顺序 NullPointer
- Android倒计时器——CountDownTimer
- Android(安卓)P WMS初始化过程
- SystemUI9.0系统应用图标加载流程
- android java代码的启动:app_process
- Android(安卓)进程间通信(IPC)
- android java代码的启动:app_process
- Android百度地图导航的那些坑
- Android(安卓)Init Language(安卓初始化语言)