1. 程式人生 > >Android RemoteViews原始碼分析以及擴充套件使用

Android RemoteViews原始碼分析以及擴充套件使用

一,寫在前面         

       在前面兩篇文章RemoteViews的基本使用(上)之通知欄 ,RemoteViews的基本使用(下)之視窗小部件 中講述了RemoteViews的兩個應用場景,這篇文章主要介紹RemoteViews的內部機制,以及一個小擴充套件,使用RemoteViews實現跨程序操作介面。本篇文章以視窗小部件為例,來分析RemoteViews如何實現跨程序操作介面。我們都知道在將小部件列表中將視窗小部件拖到桌面,會呼叫onUpdate方法,在該方法中會呼叫AppWidgetManager.updateAppWidget(appWidgetIds,remoteViews)來更新視窗小部件,呼叫RemoteViews方法的一些set..方法,修改視窗小部件的介面。對於這些不是很清楚的哥們,可以檢視文章

RemoteViews的基本使用(下)之視窗小部件 ,這篇文章對視窗小部件做了簡單的介紹,本篇文章主要從原始碼角度分析RemoteViews,對視窗小部件的生命週期以及使用不再闡述。

二,以視窗小部件為例

檢視AppWidgetManager$updateAppWidget原始碼:
public void updateAppWidget(int[] appWidgetIds, RemoteViews views) {
        try {
            sService.updateAppWidgetIds(appWidgetIds, views, mContext.getUserId());
        }
        catch (RemoteException e) {
            throw new RuntimeException("system server dead?", e);
        }
}


public static AppWidgetManager getInstance(Context context) {
        synchronized (sManagerCache) {
            if (sService == null) {
                IBinder b = ServiceManager.getService(Context.APPWIDGET_SERVICE);
                sService = IAppWidgetService.Stub.asInterface(b);
            }

            WeakReference<AppWidgetManager> ref = sManagerCache.get(context);
            AppWidgetManager result = null;
            if (ref != null) {
                result = ref.get();
            }
            if (result == null) {
                result = new AppWidgetManager(context);
                sManagerCache.put(context, new WeakReference<AppWidgetManager>(result));
            }
            return result;
        }
}
        sService是一個代理物件,updateAppWidgetIds方法的真正呼叫在服務裡,IAppWidgetService是一個AIDL介面,需要找到繼承IAppWidgetService.Stub的那個類,這裡直接告訴大家該類是AppWidgetService。        檢視AppWidgetService$updateAppWidgetIds原始碼:
public void updateAppWidgetIds(int[] appWidgetIds, RemoteViews views) {
        if (appWidgetIds == null) {
            return;
        }
        if (appWidgetIds.length == 0) {
            return;
        }
        final int N = appWidgetIds.length;

        synchronized (mAppWidgetIds) {
            for (int i=0; i<N; i++) {
                AppWidgetId id = lookupAppWidgetIdLocked(appWidgetIds[i]);
                updateAppWidgetInstanceLocked(id, views);
            }
        }
}

//進入updateAppWidgetInstanceLocked方法
void updateAppWidgetInstanceLocked(AppWidgetId id, RemoteViews views) {
        // allow for stale appWidgetIds and other badness
        // lookup also checks that the calling process can access the appWidgetId
        // drop unbound appWidgetIds (shouldn't be possible under normal circumstances)
        if (id != null && id.provider != null && !id.provider.zombie && !id.host.zombie) {
            id.views = views;

            // is anyone listening?
            if (id.host.callbacks != null) {
                try {
                    // the lock is held, but this is a oneway call
                    id.host.callbacks.updateAppWidget(id.appWidgetId, views);
                } catch (RemoteException e) {
                    // It failed; remove the callback. No need to prune because
                    // we know that this host is still referenced by this instance.
                    id.host.callbacks = null;
                }
            }
        }
}

//callbacks例項化的位置
public int[] startListening(IAppWidgetHost callbacks, String packageName, int hostId,
            List<RemoteViews> updatedViews) {
        int callingUid = enforceCallingUid(packageName);
        synchronized (mAppWidgetIds) {
            Host host = lookupOrAddHostLocked(callingUid, packageName, hostId);
            host.callbacks = callbacks;

            updatedViews.clear();

            ArrayList<AppWidgetId> instances = host.instances;
            int N = instances.size();
            int[] updatedIds = new int[N];
            for (int i=0; i<N; i++) {
                AppWidgetId id = instances.get(i);
                updatedIds[i] = id.appWidgetId;
                updatedViews.add(id.views);
            }
            return updatedIds;
        }
}
        最後會呼叫id.host.callbacks.updateAppWidget(id.appWidgetId, views),需要找到callbacks的例項化位置,上面程式碼已經給出答案,呼叫AppWidgetService$startListening方法會例項化callbacks物件。那麼,誰呼叫了AppWidgetService$startListening方法呢。        檢視類AppWidgetHost$startListening方法,原始碼如下:
public void startListening() {
        int[] updatedIds;
        ArrayList<RemoteViews> updatedViews = new ArrayList<RemoteViews>();
        
        try {
            if (mPackageName == null) {
                mPackageName = mContext.getPackageName();
            }
            updatedIds = sService.startListening(mCallbacks, mPackageName, mHostId, updatedViews);
        }
        catch (RemoteException e) {
            throw new RuntimeException("system server dead?", e);
        }

        final int N = updatedIds.length;
        for (int i=0; i<N; i++) {
            updateAppWidgetView(updatedIds[i], updatedViews.get(i));
        }
}
       sService物件就是AppWidgetService,代理物件sService.startListening(mCallbacks, mPackageName, mHostId, updatedViews)的呼叫,基於底層Binder機制,呼叫遠端服務的startListening方法,也就是AppWidgetService$startListening。       我們可以檢視sService例項化的位置,AppWidgetHost建構函式原始碼如下:
int mHostId;
    Callbacks mCallbacks = new Callbacks();
    final HashMap<Integer,AppWidgetHostView> mViews = new HashMap<Integer, AppWidgetHostView>();

    public AppWidgetHost(Context context, int hostId) {
        mContext = context;
        mHostId = hostId;
        mHandler = new UpdateHandler(context.getMainLooper());
        synchronized (sServiceLock) {
            if (sService == null) {
                IBinder b = ServiceManager.getService(Context.APPWIDGET_SERVICE);
                sService = IAppWidgetService.Stub.asInterface(b);
            }
        }
    }
        sService = IAppWidgetService.Stub.asInterface(b);獲取的不就是AppWidgetService的代理物件麼。         注意這樣一行程式碼:Callbacks mCallbacks = new Callbacks(),mCallbacks 就是updatedIds = sService.startListening(mCallbacks, mPackageName, mHostId, updatedViews)中的mCallbacks引數。通過Binder機制,呼叫遠端服務方法,即,AppWidgetService$startListening(IAppWidgetHost callbacks, String packageName, int hostId,List<RemoteViews> updatedViews),前面提到的mCallbacks引數,傳遞過來就是callbacks。這樣,終於找到了mCallbacks例項化的位置,它是AppwidgetHost裡面的一個內部類Callbacks。        前面講到更新視窗小部件,需要呼叫appWidgetManager.updateAppwidget(ids,remoteviews),最後會呼叫id.host.callbacks.updateAppWidget(id.appWidgetId, views),現在我們知道了callbacks的例項化位置,可以檢視方法updateAppWidget裡面到底做了些什麼操作。       檢視AppWidgetHost$Callbacks內部類原始碼如下:
    class Callbacks extends IAppWidgetHost.Stub {
        public void updateAppWidget(int appWidgetId, RemoteViews views) {
            Message msg = mHandler.obtainMessage(HANDLE_UPDATE);
            msg.arg1 = appWidgetId;
            msg.obj = views;
            msg.sendToTarget();
        }

        public void providerChanged(int appWidgetId, AppWidgetProviderInfo info) {
            Message msg = mHandler.obtainMessage(HANDLE_PROVIDER_CHANGED);
            msg.arg1 = appWidgetId;
            msg.obj = info;
            msg.sendToTarget();
        }
    }
      程式碼比較簡單,傳送了一個訊息到訊息佇列裡,接下來是處理訊息,原始碼如下:
class UpdateHandler extends Handler {
        public UpdateHandler(Looper looper) {
            super(looper);
        }
        
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case HANDLE_UPDATE: {
                    updateAppWidgetView(msg.arg1, (RemoteViews)msg.obj);
                    break;
                }
                case HANDLE_PROVIDER_CHANGED: {
                    onProviderChanged(msg.arg1, (AppWidgetProviderInfo)msg.obj);
                    break;
                }
            }
        }
}

//繼續檢視方法updateAppWidgetView

void updateAppWidgetView(int appWidgetId, RemoteViews views) {
        AppWidgetHostView v;
        synchronized (mViews) {
            v = mViews.get(appWidgetId);
        }
        if (v != null) {
            v.updateAppWidget(views);
        }
}
       這裡的v就是AppWidgetHostView,繼續檢視AppWidgetHostView$updateAppWidget原始碼如下:
/**
     * Process a set of {@link RemoteViews} coming in as an update from the
     * AppWidget provider. Will animate into these new views as needed
     */
    public void updateAppWidget(RemoteViews remoteViews) {
        if (LOGD) Log.d(TAG, "updateAppWidget called mOld=" + mOld);

        boolean recycled = false;
        View content = null;
        Exception exception = null;
        
        // Capture the old view into a bitmap so we can do the crossfade.
        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
            // inflate any requested LayoutParams.
            mRemoteContext = getRemoteContext(remoteViews);
            int layoutId = remoteViews.getLayoutId();

            // If our stale view has been prepared to match active, and the new
            // layout matches, try recycling it
            if (content == null && layoutId == mLayoutId) {
                try {
                    remoteViews.reapply(mContext, mView);
                    content = mView;
                    recycled = true;
                    if (LOGD) Log.d(TAG, "was able to recycled existing layout");
                } catch (RuntimeException e) {
                    exception = e;
                }
            }
            
            // Try normal RemoteView inflation
            if (content == null) {
                try {
                    content = remoteViews.apply(mContext, this);
                    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();
            }
        }
    }
         注意看49行,61行,分別呼叫remoteViews.reapply(mContext, mView),content = remoteViews.apply(mContext, this),呼叫了RemoteViews的apply載入或更新介面,呼叫RemoteViews的reapply方法更新介面,但不能載入。以本篇文章為例,介面指的是在launcher應用上顯示的視窗小部件。          在84行,執行了this.addView(content),這個this就是AppWidgetHostView,也就是說視窗小部件的介面新增在AppWidgetHostView裡,也可以理解為AppWidgetHostView是視窗小部件的父容器。理解這個很重要,後面擴充套件使用的原理就來自於此。      

三,小結

        開發一個視窗小部件,我們需要在應用中建立AppWidgetProvider的子類,並重寫一些生命週期的方法。在重寫的onUpdate方法中,呼叫appWidgetManager.updateAppWidget(ids,remoteviews)更新視窗小部件,引數remoteViews一般會呼叫一些setXXX方法來確定如何更新介面。這裡的remoteViews是在開發的應用中的,而視窗小部件的介面更新並不是在本應用中的,它的更新操作是放在SystemServer程序中。         所以視窗小部件的實現體現了一個非常重要的需求:跨程序更新介面。從上面的程式碼分析可知,介面具體更新是交給AppWidgetService處理,它是一個系統服務,開機啟動就會執行。系統服務AppWidgetService處理介面的更新,最終會呼叫remoteViews$apply方法更新介面。這個remoteViews從本應用通過Binder機制,跨程序傳遞給了AppWidgetService,我們可以猜測它實現了Parcelable介面,事實也確實如此。         因此可以得出這樣一個結論:若需要實現操作遠端介面,首先需要呼叫apply方法並返回一個View,然後將該View新增到父容器中。

四,另外

        前面程式碼這樣那樣跟下來,更新介面,最終是呼叫了Remoteviews$apply/reapply方法,那麼apply是如何更新介面的呢?下面的分析才是本篇文章的重點,上面一系列程式碼跟進只是引出這個引子,並沒有多大實際意義。只需要知道最終呼叫AppWidgetHostView$updateAppWidget方法,裡面呼叫RemoteViews的方法apply/reapply更新介面,且AppWidgetHostView是更新介面的父容器。

五,RemoteViews原始碼分析

        在分析RemoteViews$apply/reapply方法前,先分析RemoteViews的一些setXXX方法,至於為啥子看完就知道啦。

5.1,分析RemoteViews$setXXX方法

以RemoteViews$setTextViewText為例進行分析,檢視原始碼如下:
    public void setTextViewText(int viewId, CharSequence text) {
        setCharSequence(viewId, "setText", text);
    }

    //繼續看setCharSequence

    public void setCharSequence(int viewId, String methodName, CharSequence value) {
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
    }
     
    //繼續看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<Action>();
        }
        mActions.add(a);

        // update the memory usage stats
        a.updateMemoryUsageEstimate(mMemoryUsageCounter);
    }
       從上面程式碼可知:setTextViewText方法做了這樣一件事,用Action將控制元件的id,text的值封裝起來,並將Action物件放入到list集合中。後面肯定要取出集合裡的元素進行相應處理,那麼是在哪呢,後面會給出答案。        檢視內部類RemoteViews$Action原始碼:
    private abstract static class Action implements Parcelable {
        public abstract void apply(View root, ViewGroup rootParent,
                OnClickHandler handler) throws ActionException;
	
	//...code
    }
       可以發現Action是一個抽象類,並實現了Parcelable介面,有一個很重要的抽象方法apply。Action有很多子類,如TextViewSizeAction,ReflectionAction等,setTextViewText方法中的Action是ReflectionAction,setTextViewTextSize方法中Action是TextViewSizeAction。也就是說,在呼叫remoteViews.setXXX方法時,對控制元件的操作的資料封裝在Action中。每呼叫一次remoteViews.setXXX方法,就將對應Action物件存入list集合中,然後統一交給AppWidgetService處理。       當然,我們還可以使用AIDL介面實現跨程序通訊,這樣我們需要定義大量的AIDL介面去替代RemoteViews$setXXX方法。同時,由於Action都存放到list集合中,在處理時只需要獲取到list集合,便可以批量處理RemoteViews更新介面的操作,不需要頻繁的進行IPC操作,提高了程式的效能。但是,在文章RemoteViews的基本使用(上)之通知欄中有講到,RemoteViews的缺陷是不能支援所有的View,它只能支援部分的佈局控制元件,部分的View。具體支援哪些,RemoteViews的基本使用(上)之通知欄有詳細介紹,這裡不再闡述。       前面講述了在list集合中存放了Action物件,那麼在哪裡取出集合裡的元素,並執行介面更新的處理呢?這就是下面要講的,繼續看下來唄~

5.2,分析RemoteViews$apply/reapply方法

      通過上面的分析,可以猜測RemoteViews$apply/reapply方法中取出了list集合裡的元素,然後執行更新操作。當然是SystemServer程序中執行的,文章前半部分已經有很詳細的分析了。       檢視RemoteViews$apply原始碼:
    public View apply(Context context, ViewGroup parent) {
        return apply(context, parent, null);
    }

    /** @hide */
    public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
        RemoteViews rvToApply = getRemoteViewsToApply(context);

        View result;

        Context c = prepareContext(context);

        LayoutInflater inflater = (LayoutInflater)
                c.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        inflater = inflater.cloneInContext(c);
        inflater.setFilter(this);

        result = inflater.inflate(rvToApply.getLayoutId(), parent, false);

        rvToApply.performApply(result, parent, handler);

        return result;
    }

    //繼續看performApply
    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);
            }
        }
    }


        result = inflater.inflate(rvToApply.getLayoutId(), parent, false)就是載入佈局檔案,建立RemoteViews物件時會給欄位mLayoutId賦值,即rvToApply.getLayoutId()的返回值;parent是AppWidgetHostView。         performApply證實了前面的猜測,遍歷list集合,取出Action物件,呼叫action.apply(...)更新視窗小部件。Action$apply是如何更新介面的呢,繼續往下看~

5.3,分析Action$apply方法

       前面以RemoteViews$setTextViewText為例,封裝更新介面資料的Action是ReflectionAction,以其為例。        原始碼如下:
private class ReflectionAction extends Action {
	ReflectionAction(int viewId, String methodName, int type, Object value) {
            this.viewId = viewId;
            this.methodName = methodName;
            this.type = type;
            this.value = value;
        }
	//...code

	@Override
        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);
            }

            Class klass = view.getClass();
            Method method;
            try {
                method = klass.getMethod(this.methodName, getParameterType());
            }
            catch (NoSuchMethodException ex) {
                throw new ActionException("view: " + klass.getName() + " doesn't have method: "
                        + this.methodName + "(" + param.getName() + ")");
            }

            if (!method.isAnnotationPresent(RemotableViewMethod.class)) {
                throw new ActionException("view: " + klass.getName()
                        + " can't use method with RemoteViews: "
                        + this.methodName + "(" + param.getName() + ")");
            }

            try {
                //noinspection ConstantIfStatement
                if (false) {
                    Log.d(LOG_TAG, "view: " + klass.getName() + " calling method: "
                        + this.methodName + "(" + param.getName() + ") with "
                        + (this.value == null ? "null" : this.value.getClass().getName()));
                }
                method.invoke(view, this.value);
            }
            catch (Exception ex) {
                throw new ActionException(ex);
            }
        }
}
       第12行,findViewById獲取控制元件引用;第20,23,43行,使用反射技術更新該控制元件。有興趣的哥們可以研究下TextViewSizeAction的apply方法,它更新介面並不是用反射,在findViewById獲取到控制元件引用後,呼叫view.setTextSize(..)更新介面。        上面大量篇幅從原始碼角度分析,視窗小部件如何更新介面,前面我們提到這裡隱藏了一個非常重要的需求:跨程序更新介面。AppWidgetService是系統服務的一種,在SystemServer程序中執行。還是以視窗小部件為例,RemoteViews通過Binder機制傳遞到AppWidgetService中,並呼叫remoteViews$apply方法更新介面,並返回一個view,最後執行appWidgetHostView.addView(view)。好了,下面會給出原始碼分析後的擴充套件使用。

六,RemoteViews的擴充套件使用

      上面通過Binder機制傳遞RemoteViews,這裡不想這麼複雜去處理,嘗試使用廣播去完成程序間傳遞RemoteViews的工作。在應用RvSender中傳送廣播,並將RemoteViews物件放入intent的extra中;在應用RvReceiver中接受廣播,取出intent中的資料,並在該應用中完成介面更新。       應用RvSender
public class MainActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
	}
	
	public void clickButton(View v) {
		//傳送廣播
		Intent intent = new Intent();
		intent.setAction("com.example.remoteview");
		RemoteViews rv = new RemoteViews(getPackageName(), R.layout.rv_layout);
		rv.setTextViewText(R.id.tv, "hello, from Sender");
		
		intent.putExtra("remoteview", rv);
		
		sendBroadcast(intent);
		
	}
}
rv_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" 
    android:gravity="center_horizontal">
    
    <ImageView 
        android:id="@+id/iv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:src="@drawable/ic_launcher"/>
    <TextView 
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/iv"
        android:layout_marginTop="20dp"
        android:textSize="24sp"
        android:textColor="#f00"
        android:background="@android:color/darker_gray"
        android:text="remoteview"/>

</RelativeLayout>

應用RvReceiver
public class MainActivity extends Activity {

	private RelativeLayout rl;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		init();
	}

	public void init() {
		IntentFilter filter = new IntentFilter();
		filter.addAction("com.example.remoteview");
		MyReceiver receiver = new MyReceiver();
		registerReceiver(receiver, filter);
		
		rl = (RelativeLayout) findViewById(R.id.rl);
	}
	
	private class MyReceiver extends BroadcastReceiver {

		@Override
		public void onReceive(Context context, Intent intent) {
			if ("com.example.remoteview".equals(intent.getAction())) {
				RemoteViews remoteViews = intent.getParcelableExtra("remoteview");
				View view = remoteViews.apply(context, rl);
				rl.addView(view);
			}
		}
	}
}
activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="24sp"
        android:text="RvReceiver" />
    
    <RelativeLayout 
        android:id="@+id/rl"
        android:layout_marginTop="120dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"></RelativeLayout>
</RelativeLayout>
      需要注意的是,應用RvReceiver中並不需要rv_layout.xml資原始檔。在應用RvSender中點選按鈕後,應用RvReceiver介面如下:

               這篇文章就分享到這裡啦,有疑問可以留言,亦可糾錯,亦可補充,互相學習...^_^