该系统更新是CM修改原生android的基础上实现的。通过分析android系统的应用源码,可以学到一些很好的思想和编程方法学。好了,废话少说,现在就开始我们的学习之旅。
首先,在开始介绍之前,我先把之前根据CMUpdater源码分析来的框图放到上来,大家先看框图,把系统更新的整体流程大体了解一下。
通过这个框图,看上去挺复杂,其实是挺简单的,就是Service检查更新,后台下载更新,包括全安装包或者是增量更新包,下载完成之后,安装更新,重启,安装完成。
现在我们从代码开始,进行分析。
首先,我们先来看一个app项目中,所有的activity和service,就是要查看AndroidManifest.xml
文件。通过分析该代码,我们看到,在CMUpdater中的入口Activity是UpdatesSettings',同时在
AndroidManifest.xml文件中,还静态注册了四个
receiver`,其中包括:
- UpdateCheckReceiver : 在该receiver中注册了两个action,其中包括BOOT_COMPLETED和CONNECTIVITY_CHANGE,该receiver用于在系统启动后或者是网络连接发生变化的时候,进行检查系统更新。
- DownloadReceiver : 该receiver也注册了两个action,包括DOWNLOAD_COMPLETE和START_DOWNLOAD,当下载完成和开始下载广播发出之后,进行相应的操作。
- NotificationClickReceiver,该广播接收器注册了一个DOWNLOAD_NOTIFICATION_CLICKED广播,当接收到下载提醒框被点击的时候,进行更新操作。
- CheckFinishReceiver : 该广播接收器用于接收UPDATE_CHECK_FINISHED广播,当检查更新完成之后,刷新列表。
同时,在该xml文件中,也注册了几个Service:
- CMDashClockExtension : 该服务用于检查时间,用户可以选择某个时间点,进行检查更新。
- UpdateCheckService : 该服务的操作就是用于检查更新,检查更新完成之后,发出检查更新完成的广播。
- DownloadService : 顾名思义,下载服务,也就是说该服务是用来下载系统更新的。
- DownloadCompleteIntentService : 下载完成服务,当下载完成之后,发出下载完成广播。
好了,现在我们先进入入口Activity来看一下。
进入到UpdateSettings.java
文件,可以看到,该类中注册了一个Receiver,该接收器的实现为:
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
//如果Action为ACTION_DOWNLOAD_STARTED,即为下载开始
if (DownloadReceiver.ACTION_DOWNLOAD_STARTED.equals(action)) {
mDownloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
mUpdateHandler.post(mUpdateProgress); //更新ProgressBar
//如果Action为ACTION_CHECK_FINISHED,即为检查更新完成
} else if (UpdateCheckService.ACTION_CHECK_FINISHED.equals(action)) {
if (mProgressDialog != null) {
mProgressDialog.dismiss();
mProgressDialog = null;
//获取系统更新的条数
int count = intent.getIntExtra(UpdateCheckService.EXTRA_NEW_UPDATE_COUNT, -1);
if (count == 0) { // 如果为0,提醒用户未找到
Toast.makeText(UpdatesSettings.this, R.string.no_updates_found,
Toast.LENGTH_SHORT).show();
} else if (count < 0) { //如果小于0,则表示检查更新失败
Toast.makeText(UpdatesSettings.this, R.string.update_check_failed,
Toast.LENGTH_LONG).show();
}
}
//如果其他都不是,表示更新完成,并且有更新,则刷新UI
updateLayout();
}
}
};
当用户点击item(MENU_REFRESH)之后,手动进行更新检查,即为checkForUpdates()
。
private void checkForUpdates() {
if (mProgressDialog != null) {
return;
}
// 未连接网络,提示用户并返回
if (!Utils.isOnline(this)) {
Toast.makeText(this, R.string.data_connection_required, Toast.LENGTH_SHORT).show();
return;
}
//弹出进度条,用户提示用户正在检查更新
mProgressDialog = new ProgressDialog(this);
mProgressDialog.setTitle(R.string.checking_for_updates);
mProgressDialog.setMessage(getString(R.string.checking_for_updates));
mProgressDialog.setIndeterminate(true);
mProgressDialog.setCancelable(true);
mProgressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
//当用户点击取消后,提醒服务程序,取消检查更新操作
Intent cancelIntent = new Intent(UpdatesSettings.this, UpdateCheckService.class);
cancelIntent.setAction(UpdateCheckService.ACTION_CANCEL_CHECK);
startService(cancelIntent);
mProgressDialog = null;
}
});
//开启服务程序进行检查更新
Intent checkIntent = new Intent(UpdatesSettings.this, UpdateCheckService.class);
checkIntent.setAction(UpdateCheckService.ACTION_CHECK);
startService(checkIntent);
mProgressDialog.show();
}
现在我们进入检查更新服务程序,即UpdateCheckService
。在服务程序的启动方法onStartCommand()
检查用户的操作是取消检查还是检查更新,如果是取消检查,就将位于请求列表中的请求删除掉。
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//取消检查更新
if (TextUtils.equals(intent.getAction(), ACTION_CANCEL_CHECK)) {
((UpdateApplication) getApplicationContext()).getQueue().cancelAll(TAG);
return START_NOT_STICKY;
}
return super.onStartCommand(intent, flags, startId);
}
接着,服务程序调用getAvailableUpdates()
来检查可用更新,我们来看一下这个方法:
private void getAvailableUpdates() {
// Get the type of update we should check for
int updateType = Utils.getUpdateType();
// Get the actual ROM Update Server URL
URI updateServerUri = getServerURI();
Log.e(TAG, updateServerUri.toString());
UpdatesJsonObjectRequest request;
try {
request = new UpdatesJsonObjectRequest(updateServerUri.toASCIIString(),
Utils.getUserAgentString(this), buildUpdateRequest(updateType), this, this);
// Set the tag for the request, reuse logging tag
request.setTag(TAG);
} catch (JSONException e) {
Log.e(TAG, "Could not build request", e);
return;
}
((UpdateApplication) getApplicationContext()).getQueue().add(request);
}
这个代码很好理解,就是封装一下请求信息,封装为请求对象,将该请求对象添加到请求队列中,等待从服务器中获取信息。请求对象的封装是通过buildUpdateRequest()
方法来实现的。
接着,服务程序通过回调函数onResponse()
来获取相应的服务器反馈信息。
@Override
public void onResponse(JSONObject jsonObject) {
int updateType = Utils.getUpdateType();
LinkedList lastUpdates = State.loadState(this);
LinkedList updates = parseJSON(jsonObject.toString(), updateType);
int newUpdates = 0, realUpdates = 0;
for (UpdateInfo ui : updates) {
if (!lastUpdates.contains(ui)) {
newUpdates++;
}
if (ui.isNewerThanInstalled()) {
realUpdates++;
}
}
Intent intent = new Intent(ACTION_CHECK_FINISHED);
intent.putExtra(EXTRA_UPDATE_COUNT, updates.size());
intent.putExtra(EXTRA_REAL_UPDATE_COUNT, realUpdates);
intent.putExtra(EXTRA_NEW_UPDATE_COUNT, newUpdates);
recordAvailableUpdates(updates, intent);
State.saveState(this, updates);
}
在该回调函数中,将更新信息放到更新列表中,同时通过调用recordAvailableUpdates()
发送actionACTION_CHECK_FINISHED
的广播给UpdateSettings中。接着UpdateSettings调用updateLayout()
更新UI,同时调用refreshPreferences()
接着调用回调函数onStartDownload()
来通知DownloadReceiver
进行下载。
我们先来看一下recordAvailableUpdates()
函数的实现:
private void recordAvailableUpdates(LinkedList availableUpdates,
Intent finishedIntent) {
if (availableUpdates == null) {
sendBroadcast(finishedIntent);
return;
}
//保存上次更新时间,然后确保启动检查更新完成是正确的
Date d = new Date();
PreferenceManager.getDefaultSharedPreferences(UpdateCheckService.this).edit()
.putLong(Constants.LAST_UPDATE_CHECK_PREF, d.getTime())
.putBoolean(Constants.BOOT_CHECK_COMPLETED, true)
.apply();
int realUpdateCount = finishedIntent.getIntExtra(EXTRA_REAL_UPDATE_COUNT, 0);
UpdateApplication app = (UpdateApplication) getApplicationContext();
// Write to log
Log.i(TAG, "The update check successfully completed at " + d + " and found "
+ availableUpdates.size() + " updates ("
+ realUpdateCount + " newer than installed)");
//如果程序被关掉了,则在提醒框中显示更新信息
if (realUpdateCount != 0 && !app.isMainActivityActive()) {
// There are updates available
// The notification should launch the main app
Intent i = new Intent(this, UpdatesSettings.class);
i.putExtra(UpdatesSettings.EXTRA_UPDATE_LIST_UPDATED, true);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, i,
PendingIntent.FLAG_ONE_SHOT);
Resources res = getResources();
String text = res.getQuantityString(R.plurals.not_new_updates_found_body,
realUpdateCount, realUpdateCount);
// Get the notification ready
Notification.Builder builder = new Notification.Builder(this)
.setSmallIcon(R.drawable.cm_updater)
.setWhen(System.currentTimeMillis())
.setTicker(res.getString(R.string.not_new_updates_found_ticker))
.setContentTitle(res.getString(R.string.not_new_updates_found_title))
.setContentText(text)
.setContentIntent(contentIntent)
.setAutoCancel(true);
LinkedList realUpdates = new LinkedList();
for (UpdateInfo ui : availableUpdates) {
if (ui.isNewerThanInstalled()) {
realUpdates.add(ui);
}
}
Collections.sort(realUpdates, new Comparator() {
@Override
public int compare(UpdateInfo lhs, UpdateInfo rhs) {
/* sort by date descending */
long lhsDate = lhs.getDate();
long rhsDate = rhs.getDate();
if (lhsDate == rhsDate) {
return 0;
}
return lhsDate < rhsDate ? 1 : -1;
}
});
Notification.InboxStyle inbox = new Notification.InboxStyle(builder)
.setBigContentTitle(text);
int added = 0, count = realUpdates.size();
for (UpdateInfo ui : realUpdates) {
if (added < EXPANDED_NOTIF_UPDATE_COUNT) {
inbox.addLine(ui.getName());
added++;
}
}
if (added != count) {
inbox.setSummaryText(res.getQuantityString(R.plurals.not_additional_count,
count - added, count - added));
}
builder.setStyle(inbox);
builder.setNumber(availableUpdates.size());
//如果更新条数为1 ,则直接发送开始下载更新的广播
if (count == 1) {
i = new Intent(this, DownloadReceiver.class);
i.setAction(DownloadReceiver.ACTION_START_DOWNLOAD);
i.putExtra(DownloadReceiver.EXTRA_UPDATE_INFO, (Parcelable) realUpdates.getFirst());
PendingIntent downloadIntent = PendingIntent.getBroadcast(this, 0, i,
PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
builder.addAction(R.drawable.ic_tab_download,
res.getString(R.string.not_action_download), downloadIntent);
}
// Trigger the notification
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
nm.notify(R.string.not_new_updates_found_title, builder.build());
}
//发送广播,提醒UpdateSettings类
sendBroadcast(finishedIntent);
}
在updateLayout()
函数中会更新UI,用户可以点击下载按钮进行更新下载。
当下载开始之后,会广播给`DownloadReceiver'接收器。
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (ACTION_START_DOWNLOAD.equals(action)) {
UpdateInfo ui = (UpdateInfo) intent.getParcelableExtra(EXTRA_UPDATE_INFO);
handleStartDownload(context, ui);
} else if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) {
long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
handleDownloadComplete(context, id);
} else if (ACTION_INSTALL_UPDATE.equals(action)) {
StatusBarManager sb = (StatusBarManager) context.getSystemService(Context.STATUS_BAR_SERVICE);
sb.collapsePanels();
String fileName = intent.getStringExtra(EXTRA_FILENAME);
try {
Utils.triggerUpdate(context, fileName);
} catch (IOException e) {
Log.e(TAG, "Unable to reboot into recovery mode", e);
Toast.makeText(context, R.string.apply_unable_to_reboot_toast,
Toast.LENGTH_SHORT).show();
Utils.cancelNotification(context);
}
}
}
在该接收器中,如果action为ACTION_START_DOWNLOAD
,则进行handleStartDownload()
操作。
private void handleStartDownload(Context context, UpdateInfo ui) {
DownloadService.start(context, ui);
}
启动DownloadService
开始操作。
public static void start(Context context, UpdateInfo ui) {
Intent intent = new Intent(context, DownloadService.class);
intent.putExtra(EXTRA_UPDATE_INFO, (Parcelable) ui);
context.startService(intent);
}
静态方法,启动DownloadService
服务。
@Override
protected void onHandleIntent(Intent intent) {
mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
mInfo = intent.getParcelableExtra(EXTRA_UPDATE_INFO);
if (mInfo == null) {
Log.e(TAG, "Intent UpdateInfo extras were null");
return;
}
try {
getIncremental();
} catch (IOException e) {
downloadFullZip();
}
}
尝试下载增量更新包,如果增量更新包报出异常,则下载全更新包。
private void downloadFullZip() {
Log.v(TAG, "Downloading full zip");
// Build the name of the file to download, adding .partial at the end. It will get
// stripped off when the download completes
String fullFilePath = "file://" + getUpdateDirectory().getAbsolutePath() +
"/" + mInfo.getFileName() + ".partial";
long downloadId = enqueueDownload(mInfo.getDownloadUrl(), fullFilePath);
// Store in shared preferences
mPrefs.edit()
.putLong(Constants.DOWNLOAD_ID, downloadId)
.putString(Constants.DOWNLOAD_MD5, mInfo.getMD5Sum())
.apply();
Utils.cancelNotification(this);
Intent intent = new Intent(DownloadReceiver.ACTION_DOWNLOAD_STARTED);
intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
sendBroadcast(intent);
}
调用enqueueDownload()
函数来获取该下载在下载队列中的id:
private long enqueueDownload(String downloadUrl, String localFilePath) {
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(downloadUrl));
String userAgent = Utils.getUserAgentString(this);
if (userAgent != null) {
request.addRequestHeader("User-Agent", userAgent);
}
request.setTitle(getString(R.string.app_name));
request.setDestinationUri(Uri.parse(localFilePath));
request.setAllowedOverRoaming(false);
request.setVisibleInDownloadsUi(false);
// TODO: this could/should be made configurable
request.setAllowedOverMetered(true);
final DownloadManager dm = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
return dm.enqueue(request);
}
该操作相当于封装请求服务器的信息,将该信息放到DownloadManager中,获取id。
接着,向DownloadReceiver发送ACTION_DOWNLOAD_STARTED
广播。
当下载完成之后,会向DownloadReceiver发送广播ACTION_DOWNLOAD_COMPLET
,当接收器接到该广播之后,则调用handleDownloadComplete(context, id)
函数来处理下载完成的安装包。
private void handleDownloadComplete(Context context, long id) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
long enqueued = prefs.getLong(Constants.DOWNLOAD_ID, -1);
if (enqueued < 0 || id < 0 || id != enqueued) {
return;
}
String downloadedMD5 = prefs.getString(Constants.DOWNLOAD_MD5, "");
String incrementalFor = prefs.getString(Constants.DOWNLOAD_INCREMENTAL_FOR, null);
// Send off to DownloadCompleteIntentService
Intent intent = new Intent(context, DownloadCompleteIntentService.class);
intent.putExtra(Constants.DOWNLOAD_ID, id);
intent.putExtra(Constants.DOWNLOAD_MD5, downloadedMD5);
intent.putExtra(Constants.DOWNLOAD_INCREMENTAL_FOR, incrementalFor);
context.startService(intent);
// Clear the shared prefs
prefs.edit()
.remove(Constants.DOWNLOAD_MD5)
.remove(Constants.DOWNLOAD_ID)
.remove(Constants.DOWNLOAD_INCREMENTAL_FOR)
.apply();
}
通过md5检查完整性,接着启动DownloadCompleteIntentService
服务来安装更新。
@Override
protected void onHandleIntent(Intent intent) {
if (!intent.hasExtra(Constants.DOWNLOAD_ID) ||
!intent.hasExtra(Constants.DOWNLOAD_MD5)) {
return;
}
long id = intent.getLongExtra(Constants.DOWNLOAD_ID, -1);
String downloadedMD5 = intent.getStringExtra(Constants.DOWNLOAD_MD5);
String incrementalFor = intent.getStringExtra(Constants.DOWNLOAD_INCREMENTAL_FOR);
Intent updateIntent = new Intent(this, UpdatesSettings.class);
updateIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
int status = fetchDownloadStatus(id);
if (status == DownloadManager.STATUS_SUCCESSFUL) {
// Get the full path name of the downloaded file and the MD5
// Strip off the .partial at the end to get the completed file
String partialFileFullPath = fetchDownloadPartialPath(id);
if (partialFileFullPath == null) {
displayErrorResult(updateIntent, R.string.unable_to_download_file);
}
String completedFileFullPath = partialFileFullPath.replace(".partial", "");
File partialFile = new File(partialFileFullPath);
File updateFile = new File(completedFileFullPath);
partialFile.renameTo(updateFile);
// Start the MD5 check of the downloaded file
if (MD5.checkMD5(downloadedMD5, updateFile)) {
// We passed. Bring the main app to the foreground and trigger download completed
updateIntent.putExtra(UpdatesSettings.EXTRA_FINISHED_DOWNLOAD_ID, id);
updateIntent.putExtra(UpdatesSettings.EXTRA_FINISHED_DOWNLOAD_PATH,
completedFileFullPath);
updateIntent.putExtra(UpdatesSettings.EXTRA_FINISHED_DOWNLOAD_INCREMENTAL_FOR,
incrementalFor);
displaySuccessResult(updateIntent, updateFile);
} else {
// We failed. Clear the file and reset everything
mDm.remove(id);
if (updateFile.exists()) {
updateFile.delete();
}
displayErrorResult(updateIntent, R.string.md5_verification_failed);
}
} else if (status == DownloadManager.STATUS_FAILED) {
// The download failed, reset
mDm.remove(id);
displayErrorResult(updateIntent, R.string.unable_to_download_file);
}
}
该服务通过封装Intent来返回UpdateSettings
中的onNewIntent()
函数来进行处理。
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
// Check if we need to refresh the screen to show new updates
if (intent.getBooleanExtra(EXTRA_UPDATE_LIST_UPDATED, false)) {
updateLayout();
}
//检查下载完成
checkForDownloadCompleted(intent);
}
我们来看一下checkForDownloadCompleted()
函数的实现:
private void checkForDownloadCompleted(Intent intent) {
if (intent == null) {
return;
}
long downloadId = intent.getLongExtra(EXTRA_FINISHED_DOWNLOAD_ID, -1);
if (downloadId < 0) {
return;
}
String fullPathName = intent.getStringExtra(EXTRA_FINISHED_DOWNLOAD_PATH);
if (fullPathName == null) {
return;
}
String fileName = new File(fullPathName).getName();
// If this is an incremental, find matching target and mark it as downloaded.
String incrementalFor = intent.getStringExtra(EXTRA_FINISHED_DOWNLOAD_INCREMENTAL_FOR);
if (incrementalFor != null) {
UpdatePreference pref = (UpdatePreference) mUpdatesList.findPreference(incrementalFor);
if (pref != null) {
pref.setStyle(UpdatePreference.STYLE_DOWNLOADED);
pref.getUpdateInfo().setFileName(fileName);
//调用更新安装
onStartUpdate(pref);
}
} else {
// Find the matching preference so we can retrieve the UpdateInfo
UpdatePreference pref = (UpdatePreference) mUpdatesList.findPreference(fileName);
if (pref != null) {
pref.setStyle(UpdatePreference.STYLE_DOWNLOADED);
onStartUpdate(pref);
}
}
resetDownloadState();
}
该函数通过调用onStartUpdate(pref)
来安装更新:
@Override
public void onStartUpdate(UpdatePreference pref) {
final UpdateInfo updateInfo = pref.getUpdateInfo();
// Prevent the dialog from being triggered more than once
if (mStartUpdateVisible) {
return;
}
mStartUpdateVisible = true;
// Get the message body right
String dialogBody = getString(R.string.apply_update_dialog_text, updateInfo.getName());
// Display the dialog
new AlertDialog.Builder(this)
.setTitle(R.string.apply_update_dialog_title)
.setMessage(dialogBody)
.setPositiveButton(R.string.dialog_update, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
//提醒用户是否安装,用户点击确定之后,triggerUpdate()
Utils.triggerUpdate(UpdatesSettings.this, updateInfo.getFileName());
} catch (IOException e) {
Log.e(TAG, "Unable to reboot into recovery mode", e);
Toast.makeText(UpdatesSettings.this, R.string.apply_unable_to_reboot_toast,
Toast.LENGTH_SHORT).show();
}
}
})
.setNegativeButton(R.string.dialog_cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mStartUpdateVisible = false;
}
})
.show();
}
提醒用户是否安装,用户点击安装之后,激发triggerUpdate()
函数进行安装:
public static void triggerUpdate(Context context, String updateFileName) throws IOException {
/*
* Should perform the following steps.
* 1.- mkdir -p /cache/recovery
* 2.- echo 'boot-recovery' > /cache/recovery/command
* 3.- if(mBackup) echo '--nandroid' >> /cache/recovery/command
* 4.- echo '--update_package=SDCARD:update.zip' >> /cache/recovery/command
* 5.- reboot recovery
*/
// Set the 'boot recovery' command
Process p = Runtime.getRuntime().exec("sh");
OutputStream os = p.getOutputStream();
os.write("mkdir -p /cache/recovery/\n".getBytes());
os.write("echo 'boot-recovery' >/cache/recovery/command\n".getBytes());
// See if backups are enabled and add the nandroid flag
/* TODO: add this back once we have a way of doing backups that is not recovery specific
if (mPrefs.getBoolean(Constants.BACKUP_PREF, true)) {
os.write("echo '--nandroid' >> /cache/recovery/command\n".getBytes());
}
*/
// Add the update folder/file name
// Emulated external storage moved to user-specific paths in 4.2
String userPath = Environment.isExternalStorageEmulated() ? ("/" + UserHandle.myUserId()) : "";
String cmd = "echo '--update_package=" + getStorageMountpoint(context) + userPath
+ "/" + Constants.UPDATES_FOLDER + "/" + updateFileName
+ "' >> /cache/recovery/command\n";
os.write(cmd.getBytes());
os.flush();
// Trigger the reboot
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
powerManager.reboot("recovery");
}
/*
* Should perform the following steps.
* 1.- mkdir -p /cache/recovery
* 2.- echo 'boot-recovery' > /cache/recovery/command
* 3.- if(mBackup) echo '--nandroid' >> /cache/recovery/command
* 4.- echo '--update_package=SDCARD:update.zip' >> /cache/recovery/command
* 5.- reboot recovery
*/
在注释中,说明了必须运行下面步骤:
- mkdir -p /cache/recovery //创建文件夹
- echo 'boot-recovery' > /cache/recovery/command
- if(mBackup) echo '--nandroid' >> /cache/recovery/command
- echo '--update_package=SDCARD:update.zip' >> /cache/recovery/command
- reboot recovery
这五个命令运行完成之后,便会重启安装系统更新了。