編輯:關於Android編程
由於公司環境惡劣,小菜鳥我本來想畫UML圖來顯示類與類之間的關系,可惜這個念頭無法達成,也只好用Word文檔來完成。待菜鳥我辭職了,再自己畫上UML圖和Gif動態圖,來顯示類於類之間的關系。所以如果有看客請諒解諒解本人的情況。
之前復習了實現桌面小部件的時候,知道了其中運用到了RemoteView這個特殊的類。這個類,顧名思義就是遠程視圖。可以跨越進程的顯示View。
這也就誕生了一些特殊應用,比如說,我們完成遠程的顯示視圖。之前復習的AIDL也可以辦到這一點,但是相比於直接使用RemoteView來說的確是復雜了那麼一點。
不過RemoteView的缺點也是很明顯的,為了提高RemoteView的進程間通訊的速度,RemoteView只支持一下幾種View以及 ViewGroup:
RemoteView在系統中主要運用的場景有:通知和桌面部件。之前對通知有了一定的研究了,這一次我們來分析桌面部件中RemoteView的應用。
首先,上一章我們提到了,在傳輸RemoteView之前,需要現在RemoteView中設置好相關的資源:
RemoteViews remoteView = new RemoteViews(context.getPackageName(),R.layout.appwidget); remoteView.setImageViewBitmap(R.id.imageView1,rotateBitmap(context,srcBitmap,degree));
那麼我們就從方法setImageViewBitmap作為研究入口開始探索。
public void setImageViewBitmap(int viewId, Bitmap bitmap) { setBitmap(viewId, "setImageBitmap", bitmap); }
這裡很簡單又調用了setBitmap
public void setBitmap(int viewId, String methodName, Bitmap value) { addAction(new BitmapReflectionAction(viewId, methodName, value)); }
接著這裡又調用了addAction的方法。在這裡我先提一提RemoteView的工作原理,這樣才好繼續理解。
前文提到了,RemoteView同樣可以向AIDL實現那樣用View去訪問遠程進程,但是原理卻完全不一樣。AIDL是通過另一端實現.Stub內部類,在其中調用遠程View數據來實現。
然而在RemoteView中,使用的策略是將實現了Parcelable接口的內部類Action傳送到遠程進程。
private abstract static class Action implements Parcelable
我們將操作的對象操作封裝到Action中,接著在遠程端依次操作Action,最後再一次跨進程回到RemoteView中讓RemoteView去修改View。這麼做的高明之處在於,省去了每一種View定義一個Binder接口,提高了程序的性能。
了解RemoteView的原理之後,讓我們看看addAction是如何實現的。
private void addAction(Action a) { if (hasLandscapeAndPortraitLayouts()) { throw new RuntimeException("RemoteViews specifying separate landscape and portrait" + " layouts cannot be modified. Instead, fully configure the landscape and" + " portrait layouts individually before constructing the combined layout."); } if (mActions == null) { mActions = new ArrayList(); } mActions.add(a); // update the memory usage stats a.updateMemoryUsageEstimate(mMemoryUsageCounter); }
從上面的源碼,可以清晰的知道,addAction是將一系列Action封裝到List中,這裡就完成了Action的存儲。
我們什麼時候調用這裡面的Action的list呢?我們之前寫AppWidget接下來是這麼寫:
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); appWidgetManager.updateAppWidget(new ComponentName(context, AppProvider.class),remoteView);
這麼寫其實就是AppWidgetManager調用AppWidgetProvider中的我們自己覆寫的onUpdate()方法。
比如說,我們研究的setBitmap方法中,傳入了方法名setImageBitmap,接著在addAction中通過反射調用這個方法。
且看在BitmapReflectionAction這個內部類中的構造類。
BitmapReflectionAction(int viewId, String methodName, Bitmap bitmap) { this.bitmap = bitmap; this.viewId = viewId; this.methodName = methodName; bitmapId = mBitmapCache.getBitmapId(bitmap); }
可以知道的,這裡將相關的數據全部存入到這個繼承了Action類的BitmapReflectionAction。
下面是AppWidgetManager的updateAppWidget方法。
public void updateAppWidget(int[] appWidgetIds, RemoteViews views) { if (mService == null) { return; } try { mService.updateAppWidgetIds(mPackageName, appWidgetIds, views); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); } }
這裡的AppWidgetManager將會在SystemServer中運行起來,這裡並不討論,先放出一點證據:
private static final String APPWIDGET_SERVICE_CLASS ="com.android.server.appwidget.AppWidgetService"; if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_APP_WIDGETS)) { mSystemServiceManager.startService(APPWIDGET_SERVICE_CLASS); }
上面那個SystemServer究竟如何啟動這裡暫不做討論,我們繼續。
在上面一段函數中mService同樣調用了 updateAppWidget,但是在這裡指的是IAppWidgetService這個Binder接口。而AppWidgetServiceImpl就是實現這個接口。
class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBackupProvider,OnCrossProfileWidgetProvidersChangeListener
AppWidgetServiceImpl中工作
我們看看這個類中updateAppWidget中調用了updateAppWidgetIds:
private void updateAppWidgetIds(String callingPackage, int[] appWidgetIds,
RemoteViews views, boolean partially) {
final int userId = UserHandle.getCallingUserId();
if (appWidgetIds == null || appWidgetIds.length == 0) {
return;
}
// Make sure the package runs under the caller uid.
mSecurityPolicy.enforceCallFromPackage(callingPackage);
final int bitmapMemoryUsage = (views != null) ? views.estimateMemoryUsage() : 0;
if (bitmapMemoryUsage > mMaxWidgetBitmapMemory) {
throw new IllegalArgumentException("RemoteViews for widget update exceeds"
+ " maximum bitmap memory usage (used: " + bitmapMemoryUsage
+ ", max: " + mMaxWidgetBitmapMemory + ")");
}
synchronized (mLock) {
ensureGroupStateLoadedLocked(userId);
final int N = appWidgetIds.length;
for (int i = 0; i < N; i++) {
final int appWidgetId = appWidgetIds[i];
// NOTE: The lookup is enforcing security across users by making
// sure the caller can only access widgets it hosts or provides.
Widget widget = lookupWidgetLocked(appWidgetId,
Binder.getCallingUid(), callingPackage);
if (widget != null) {
updateAppWidgetInstanceLocked(widget, views, partially);
}
}
}
}
上一段代碼就是整個AppWidget的核心邏輯,上面做的事情有以下幾件:
1.final int userId = UserHandle.getCallingUserId();獲取uid(用來識別程序的ID)
2.mSecurityPolicy.enforceCallFromPackage(callingPackage);保證package中運行的是我們的程序
ensureGroupStateLoadedLocked(userId);做的事情有點多,簡單說就是從uid獲取到程序的路徑以及相關文件的路徑的,獲取到Widget的實例,並且將Widget實例添加到ArrayList中。
updateAppWidgetInstanceLocked(widget, views, partially);widget的實例不為空的時候,則更新或者載入View。
我們先看看ensureGroupStateLoadedLocked(userId)做了什麼:
private void ensureGroupStateLoadedLocked(int userId) {
final int[] profileIds = mSecurityPolicy.getEnabledGroupProfileIds(userId);
// Careful lad, we may have already loaded the state for some
// group members, so check before loading and read only the
// state for the new member(s).
int newMemberCount = 0;
final int profileIdCount = profileIds.length;
for (int i = 0; i < profileIdCount; i++) {
final int profileId = profileIds[i];
if (mLoadedUserIds.indexOfKey(profileId) >= 0) {
profileIds[i] = LOADED_PROFILE_ID;
} else {
newMemberCount++;
}
}
if (newMemberCount <= 0) {
return;
}
int newMemberIndex = 0;
final int[] newProfileIds = new int[newMemberCount];
for (int i = 0; i < profileIdCount; i++) {
final int profileId = profileIds[i];
if (profileId != LOADED_PROFILE_ID) {
mLoadedUserIds.put(profileId, profileId);
newProfileIds[newMemberIndex] = profileId;
newMemberIndex++;
}
}
clearProvidersAndHostsTagsLocked();
loadGroupWidgetProvidersLocked(newProfileIds);
loadGroupStateLocked(newProfileIds);
}
在這裡面工作的事情主要有兩個:
1.loadGroupWidgetProvidersLocked(newProfileIds):將從xml中通過標簽讀取Provider到list中
2.loadGroupStateLocked(newProfileIds):通過讀取之前定義的appwidget_info中的信息後,實例化widget加入到widget的list中。
檢查完組件的狀態之後,我們就應該做出更新的相應update動作,updateAppWidgetIds裡面調用了updateAppWidgetInstanceLocked:
private void updateAppWidgetInstanceLocked(Widget widget, RemoteViews views,
boolean isPartialUpdate) {
if (widget != null && widget.provider != null
&& !widget.provider.zombie && !widget.host.zombie) {
if (isPartialUpdate && widget.views != null) {
// For a partial update, we merge the new RemoteViews with the old.
widget.views.mergeRemoteViews(views);
} else {
// For a full update we replace the RemoteViews completely.
widget.views = views;
}
scheduleNotifyUpdateAppWidgetLocked(widget, views);
}
}
判斷是否是部分widget的刷新,假如是部分刷新以及widget中實例為空,則操作其中RemoteView中的Action的list,接著交給scheduleNotifyUpdateAppWidgetLocked做核心工作。
private void scheduleNotifyUpdateAppWidgetLocked(Widget widget, RemoteViews updateViews) {
if (widget == null || widget.provider == null || widget.provider.zombie
|| widget.host.callbacks == null || widget.host.zombie) {
return;
}
SomeArgs args = SomeArgs.obtain();
args.arg1 = widget.host;
args.arg2 = widget.host.callbacks;
args.arg3 = updateViews;
args.argi1 = widget.appWidgetId;
mCallbackHandler.obtainMessage(
CallbackHandler.MSG_NOTIFY_UPDATE_APP_WIDGET,
args).sendToTarget();
}
接下來就發送信息CallbackHandler.MSG_NOTIFY_UPDATE_APP_WIDGET,將工作交給mCallbackHandler這個Handler對象工作。
讓我們看看這個Handler中handleMessage究竟完成了什麼:
case MSG_NOTIFY_UPDATE_APP_WIDGET: {
SomeArgs args = (SomeArgs) message.obj;
Host host = (Host) args.arg1;
IAppWidgetHost callbacks = (IAppWidgetHost) args.arg2;
RemoteViews views = (RemoteViews) args.arg3;
final int appWidgetId = args.argi1;
args.recycle();
handleNotifyUpdateAppWidget(host, callbacks, appWidgetId, views);
} break;
可以知道我們最後將工作交給handleNotifyUpdateAppWidget()方法。
private void handleNotifyUpdateAppWidget(Host host, IAppWidgetHost callbacks,
int appWidgetId, RemoteViews views) {
try {
callbacks.updateAppWidget(appWidgetId, views);
} catch (RemoteException re) {
synchronized (mLock) {
Slog.e(TAG, "Widget host dead: " + host.id, re);
host.callbacks = null;
}
}
}
最後還是調用callbacks.updateAppWidget,而callbacks就是IAppWidgetHost這個Binder接口。
AppWidgetHost中的工作
而IAppWidgetHost的具體實現是AppWidgetHost中的Callback內部類,這個時候我們已經從SystemServer的進程中回到了我們的自己的進程,接著再通過消息機制,調用函數:
void updateAppWidgetView(int appWidgetId, RemoteViews views) {
AppWidgetHostView v;
synchronized (mViews) {
v = mViews.get(appWidgetId);
}
if (v != null) {
v.updateAppWidget(views);
}
}
可以看見這裡就調用AppWidgetHostView裡面的update方法。
AppWidgetHostView中的流程
可以說,接下來這個類就是真正執行更新的類。我們先去看看這個類中的update方法。
先聲明AppWidgetHostView就是remoteView父容器,它是繼承於FrameLayout,也就是說,它擁有FrameLayout中的特性,這樣我們其實可以做很多事情了。
public class AppWidgetHostView extends FrameLayout
我們繼續看看updateAppWidget中的方法:
public void updateAppWidget(RemoteViews remoteViews) {
if (LOGD) Log.d(TAG, "updateAppWidget called mOld=" + mOld);
boolean recycled = false;
View content = null;
Exception exception = null;
// 插入以前的view到bitmap讓我們可以辦到淡入淡出效果
if (CROSSFADE) {
if (mFadeStartTime < 0) {
if (mView != null) {
final int width = mView.getWidth();
final int height = mView.getHeight();
try {
mOld = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
} catch (OutOfMemoryError e) {
// we just won't do the fade
mOld = null;
}
if (mOld != null) {
//mView.drawIntoBitmap(mOld);
}
}
}
}
if (remoteViews == null) {
if (mViewMode == VIEW_MODE_DEFAULT) {
// We've already done this -- nothing to do.
return;
}
content = getDefaultView();
mLayoutId = -1;
mViewMode = VIEW_MODE_DEFAULT;
} else {
// Prepare a local reference to the remote Context so we're ready to准備一個本地的應用給遠程Context
// inflate any requested LayoutParams.
mRemoteContext = getRemoteContext();
int layoutId = remoteViews.getLayoutId();
// 大概是如果舊的布局和新的布局相匹配,則重新用回原來的remoteView
//
if (content == null && layoutId == mLayoutId) {
try {
remoteViews.reapply(mContext, mView, mOnClickHandler);
content = mView;
recycled = true;
if (LOGD) Log.d(TAG, "was able to recycled existing layout");
} catch (RuntimeException e) {
exception = e;
}
}
// Try normal RemoteView inflation嘗試著加載遠程視圖remoteview
if (content == null) {
try {
content = remoteViews.apply(mContext, this, mOnClickHandler);
if (LOGD) Log.d(TAG, "had to inflate new layout");
} catch (RuntimeException e) {
exception = e;
}
}
mLayoutId = layoutId;
mViewMode = VIEW_MODE_CONTENT;
}
if (content == null) {
if (mViewMode == VIEW_MODE_ERROR) {
// We've already done this -- nothing to do.
return ;
}
Log.w(TAG, "updateAppWidget couldn't find any view, using error view", exception);
content = getErrorView();
mViewMode = VIEW_MODE_ERROR;
}
if (!recycled) {
prepareView(content);
addView(content);
}
if (mView != content) {
removeView(mView);
mView = content;
}
if (CROSSFADE) {
if (mFadeStartTime < 0) {
// if there is already an animation in progress, don't do anything --
// the new view will pop in on top of the old one during the cross fade,
// and that looks okay.
mFadeStartTime = SystemClock.uptimeMillis();
invalidate();
}
}
}
上面的做的事情主要有兩個:
1.如果過去的布局(layout)和新載入的布局(layout)相匹配則舊的重用,調用remoteView.reapply
2.如果過去的布局(layout)和新的不匹配,則調用remoteView.apply
我們這裡只討論第一次加載的情況,因此繼續看apply方法。感興趣的,可以起自行去看看reapply方法的內容。
回到RemoteView工作
在remoteViews.apply(mContext, this, mOnClickHandler)函數中調用了apply:
public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
RemoteViews rvToApply = getRemoteViewsToApply(context);
View result;
// RemoteViews may be built by an application installed in another
// user. So build a context that loads resources from that user but
// still returns the current users userId so settings like data / time formats
// are loaded without requiring cross user persmissions.
final Context contextForResources = getContextForResources(context);
Context inflationContext = new ContextWrapper(context) {
@Override
public Resources getResources() {
return contextForResources.getResources();
}
@Override
public Resources.Theme getTheme() {
return contextForResources.getTheme();
}
};
LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
// Clone inflater so we load resources from correct context and
// we don't add a filter to the static version returned by getSystemService.
inflater = inflater.cloneInContext(inflationContext);
inflater.setFilter(this);
result = inflater.inflate(rvToApply.getLayoutId(), parent, false);
rvToApply.performApply(result, parent, handler);
return result;
}
上面的代碼可以看出我們是通過LayoutInfater動態加載RemoteView,加載布局文件可以動過rvToApply.getLayoutId()獲得的。加載好文件之後,調用rvToApply.performApply(result, parent, handler)去執行具體的更新操作。
private void performApply(View v, ViewGroup parent, OnClickHandler handler) {
if (mActions != null) {
handler = handler == null ? DEFAULT_ON_CLICK_HANDLER : handler;
final int count = mActions.size();
for (int i = 0; i < count; i++) {
Action a = mActions.get(i);
a.apply(v, parent, handler);
}
}
}
可以看到的是這個時候,我們獲取list中的Action,在這裡執行具體的對象對應具體的Action操作。這就完成了,我們不需要將View數據跨越進程的修改操作,而是在本線程進行真正的修改。
此時,存在隊列中的ReflectionAction將會調用自身的apply,再通過反射去調用存在其中的方法名。
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
final View view = root.findViewById(viewId);
if (view == null) return;
Class param = getParameterType();
if (param == null) {
throw new ActionException("bad type: " + this.type);
}
try {
getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value));
} catch (ActionException e) {
throw e;
} catch (Exception ex) {
throw new ActionException(ex);
}
}
這個時候我們發現將會調用ImageView中的setImageBitmap方法。
大致上,RemoteView的工作流程就完成了。同理在Notification中也是類似的思路,有興趣的讀者可以自己去看看。
RemoteView的使用以及意義
RemoteView可以作為一種簡化後的可以跨進程UI更新的方案。下面是一個模擬通知框的遠程修改UI的簡單Demo,這一次我就借花獻佛,借用任玉剛大神的Demo:
我們首先建立兩個Activity,一個遠程,一個本地,只需要在< activity >標簽下添加屬性“:remote”即可。
我們先看發送端:
DemoActivity_2.java:
public class DemoActivity_2 extends Activity {
private static final String TAG = "DemoActivity_2";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.demo_2);
Log.d(TAG, "onCreate");
Toast.makeText(this, getIntent().getStringExtra("sid"),
Toast.LENGTH_SHORT).show();
initView();
}
private void initView() {
}
public void onButtonClick(View v) {
//加載RemoteView布局文件
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_simulated_notification);
//加載資源文件
remoteViews.setTextViewText(R.id.msg, "msg from process:" + Process.myPid());
remoteViews.setImageViewResource(R.id.icon, R.drawable.icon1);
//聲明pendingintent是啟動activity
PendingIntent pendingIntent = PendingIntent.getActivity(this,
0, new Intent(this, DemoActivity_1.class), PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent openActivity2PendingIntent = PendingIntent.getActivity(
this, 0, new Intent(this, DemoActivity_2.class), PendingIntent.FLAG_UPDATE_CURRENT);
//給控件綁定pendingIntent
remoteViews.setOnClickPendingIntent(R.id.item_holder, pendingIntent);
remoteViews.setOnClickPendingIntent(R.id.open_activity2, openActivity2PendingIntent);
Intent intent = new Intent(MyConstants.REMOTE_ACTION);
intent.putExtra(MyConstants.EXTRA_REMOTE_VIEWS, remoteViews);
//發送廣播
sendBroadcast(intent);
}
}
接下來是接收端MainActivity:
public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
private LinearLayout mRemoteViewsContent;
//類似像桌面小部件一樣,做一個receiver來接受廣播
private BroadcastReceiver mRemoteViewsReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
RemoteViews remoteViews = intent
.getParcelableExtra(MyConstants.EXTRA_REMOTE_VIEWS);
if (remoteViews != null) {
//發送來的remoteview不為空時更新
updateUI(remoteViews);
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
mRemoteViewsContent = (LinearLayout) findViewById(R.id.remote_views_content);
IntentFilter filter = new IntentFilter(MyConstants.REMOTE_ACTION);
registerReceiver(mRemoteViewsReceiver, filter);
}
private void updateUI(RemoteViews remoteViews) {
// View view = remoteViews.apply(this, mRemoteViewsContent);
//通過方法getIdentifier來加載相應名字的layout布局
int layoutId = getResources().getIdentifier("layout_simulated_notification", "layout", getPackageName());
View view = getLayoutInflater().inflate(layoutId, mRemoteViewsContent, false);
//調用reapply更新remoteView
remoteViews.reapply(this, view);
mRemoteViewsContent.addView(view);
}
@Override
protected void onDestroy() {
unregisterReceiver(mRemoteViewsReceiver);
super.onDestroy();
}
public void onButtonClick(View v) {
if (v.getId() == R.id.button1) {
Intent intent = new Intent(this, TestActivity.class);
startActivity(intent);
} else if (v.getId() == R.id.button2) {
Intent intent = new Intent(this, DemoActivity_2.class);
startActivity(intent);
}
}
}
這樣就完成一次跨進程的UI更新,是不是覺得比使用AIDL簡單多了呢?注意這裡要更新的話,必須使用remoteView支持的view和viewgroup
代碼下載:Github
RemoteView流程圖與機制
RemoteView機制:
RemoteView的View結構:
RemoteView工作流程圖:
這樣RemoteView的工作流程大致分析完了。當然裡面不僅僅只有這麼多,裡面涉及到的Service不僅僅只有一個AppManagerService還有PackageService,UserService等等,更加詳細的,讀者感興趣的可以去自行查看源碼,這裡只給出了大致脈絡,以及主要流程。
感謝任玉剛大神的android開發探索藝術,幫助了我看代碼。
主要介紹除了常規的kernel的printk和android的DDMS, logcat外的幾個調試手段. 包括bugreport, oprofile, traceview
沒睡著覺,起來更篇文章吧哈哈!首先祝賀李宗偉擊敗我丹,雖然我是支持我丹的,但是他也不容易哈哈,值得尊敬的人!切入正題:這一篇來介紹個自定義廣播接收者。通常我們在外撥電話的
搭建JAVA開發環境根據操作系統分為x86或x64位的,下載jdk1.8以上的版本,本機安裝時的java版本:jdk-8u45-windows-x64.exe配置JAVA
導語 實現了教務系統中課程的導入,分類顯示課程。學期的修改,增加,修改。課程按照周的顯示。課程修改上課星期和上課周。上課課程的自動歸類。一、主要功能界面 開發過程一開始