Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 關於RemoteView的一點愚見(RemoteView在AppWidget中的工作流程)

關於RemoteView的一點愚見(RemoteView在AppWidget中的工作流程)

編輯:關於Android編程

前言

由於公司環境惡劣,小菜鳥我本來想畫UML圖來顯示類與類之間的關系,可惜這個念頭無法達成,也只好用Word文檔來完成。待菜鳥我辭職了,再自己畫上UML圖和Gif動態圖,來顯示類於類之間的關系。所以如果有看客請諒解諒解本人的情況。

正文

之前復習了實現桌面小部件的時候,知道了其中運用到了RemoteView這個特殊的類。這個類,顧名思義就是遠程視圖。可以跨越進程的顯示View。
這也就誕生了一些特殊應用,比如說,我們完成遠程的顯示視圖。之前復習的AIDL也可以辦到這一點,但是相比於直接使用RemoteView來說的確是復雜了那麼一點。
不過RemoteView的缺點也是很明顯的,為了提高RemoteView的進程間通訊的速度,RemoteView只支持一下幾種View以及 ViewGroup:

Layout:
FrameLayout,LinearLayout,RelativeLayout,GridLayout View:
AnalogClock,Button,Chronometer,ImageButton,ImageView,ProgressBar
,TextView,ViewFlipper,ListView,GridView,StackView,AdapterViewFilter,
ViewStub。

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中的set工作

了解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工作

下面是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機制

RemoteView的View結構:
RemoteView的View結構

RemoteView工作流程圖:
RemoteView工作流程

這樣RemoteView的工作流程大致分析完了。當然裡面不僅僅只有這麼多,裡面涉及到的Service不僅僅只有一個AppManagerService還有PackageService,UserService等等,更加詳細的,讀者感興趣的可以去自行查看源碼,這裡只給出了大致脈絡,以及主要流程。

感謝任玉剛大神的android開發探索藝術,幫助了我看代碼。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved