美文网首页源码解析代码改变世界Android知识
Android Widget 源码解析——锁屏为例

Android Widget 源码解析——锁屏为例

作者: 福尔摩斯春卷 | 来源:发表于2014-12-20 15:22 被阅读1964次

    本文主要从系统层怎样加载一个widget分析,不包含怎样创建一个含有widget的app。
    所谓widget,梗概流程就是App开发者传给系统一个自定义的RemoteView,锁屏或者桌面把这个RemotView解析出来放在自己的界面上,以widget的形式显示出来。
    文章有点长,没耐心只想看大概结构可以翻到最后看小结中的图。

    widget相关类

    相关的类主要有以下几个

    AppWidgetHostView

    AppWodgetHostView是一个View,是容纳App传来的View的容器。
    由于RemoteView并不是真正的View,只是一个View的描述,所以需要通过updateAppWidget(RemoteViews views)这个方法把View创建出来,然后加到AppWidgetView里。

        public void updateAppWidget(RemoteViews remoteViews) {
    
            if (LOGD) Log.d(TAG, "updateAppWidget called mOld=" + mOld);
    
                   ...//此处有省略,详见源码
                // Try normal RemoteView inflation
                if (content == null) {
                    try {
                        /// M: add for using customer view, migrated from GB to ICS to JB
                        remoteViews.setHasUsedCustomerView(mInfo.hasUsedCustomerView);
                        content = remoteViews.apply(mContext, this, mOnClickHandler);//重点,在这里apply"
                        if (LOGD) Log.d(TAG, "had to inflate new layout");
                    } catch (RuntimeException e) {
                        exception = e;
                    }
                }
    
            ...//此处有省略,详见源码
    
            if (!recycled) {
                prepareView(content);
                addView(content);
            }
    
            ...//此处有省略,详见源码
        }
        
    
    • 以上代码中的mView就是要加到AppWidgetHostView中的View,remoteViews.apply创建了实际的View.
    AppWidgetHost

    是容纳appwidget的地方,它有以下功能:

    • 监听来自AppWidgetService的事件,这是主要处理update和provider_changed两个事件,根据这两个事件更新widget。(此处加代码)
    • 另外一个功能就是创建AppWidgetHostView。这里先创建AppWidgetHostView,然后通过AppWidgetService查询 appWidgetId对应的RemoteViews,最后把RemoteViews传递给AppWidgetHostView去 updateAppWidget。(此处加代码)

    以上两个类是基类,用户可以根据这两个类来定制自己的widget的。

    AppWidgetProvider

    AppWidgetProvider,其本质BroadcastReceiver。用户用这个类去创建自己的widget,用户可以通过继承AppwidgetProvider的一个或者几个方法来定义自己的widget以及控制widget的更新
    主要的function以及作用可以参照开发者文档

    AppWidgetService

    Widget的核心类,首先在系统启动以后在systemReady做了以下工作:

    • loadAppWidgetListLocked()通过PackageManager从Android系统中查找所有已经被安装的AppWidget(包含"android.appwidget.action.APPWIDGET_UPDATE" 的Action和meta-data标签),解析AppWidget的配置信息,封闭成对象,保存到List集合。
    • addProviderLocked 从/data/system/appwidgets.xml文件读取已经被添加到Launcher的AppWidget信息,封闭成对象,保存到List集合中。
    • systemRunning(boolean safeMode) 注册四个广播接收器:第一. Android系统启动完成,第二. Android配置信息改变,第三. 添加删除应用,第四. sdcard的安装与缷载。
      AppWidgetService承担着所有widget的管理工作。Widget安装,删除,更新等等都需要经过AppWidgetService,它是开机就启动的.
    AppWidgetManager

    Updates AppWidget state; gets information about installed AppWidget providers and other AppWidget related state.
    这是代码中对这个类的注释,可以看出,这个类的主要作用是更新AppWidget的状态以及得到appwidget的信息。

    • 这个类中定义了一些很重要的intent,例如

      • ACTION_KEYGUARD_APPWIDGET_PICK:Setting中创建这个Activity,会列出当前系统中可以供添加到锁屏的所有widget,用户可以在列表中选择一个widget添加到锁屏上。具体实现参照Setting中的类KeyguardAppWidgetPickActivity。
      • ACTION_APPWIDGET_BIND:允许绑定一个widget到系统中。参见Setting中的AllowBindAppWidgetActivity
        等等。
    • 另外这个类中有一些很重要的接口,定义了widget的加载。更新等操作,如

      • updateAppWidget(int appWidgetId, RemoteViews views)//更新widget,在第一次加载widget的时候,加载widget
      • notifyAppWidgetViewDataChanged(int[] appWidgetIds, int viewId)//监听widget的改变。
      • bindAppWidgetId(int appWidgetId, ComponentName provider) //绑定widget到系统中。需要BIND_APPWIDGET权限
        接口的实现都在AppWidgetService中,使用这个类的主要是锁屏和桌面。
    AppWidgetProviderInfo

    每个AppWidget都有一个AppWidgetProviderInfo对象,该对象描述了每个AppWidget的基本数据(meta-data)信息 ,其定义在<appwidget-provider>节点信息。

    示例如下:

    <?xml version="1.0" encoding="utf-8"?>
    <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
        android:minWidth="100dp"
        android:minHeight="40dp"
            //设置更新时间  毫秒单位
        android:updatePeriodMillis="86400000"
            //引用的布局文件
        android:initialLayout="@layout/widget_layout"
    >
    </appwidget-provider>
    
    

    不做赘述。


    存储位置:

    上文可知,系统中所有的widget是通过packageManager从package里查找的(包含"android.appwidget.action.APPWIDGET_UPDATE" 的Action和meta-data标签),那么已经被添加的锁屏的widget的信息或者id存储在哪里呢?
    我们再来看KeyguardAppWidgetPickActivity,在添加appwidget以后选择一个widget的时候会调到
    mLockPatternUtils.addAppWidget(appWidgetId, 0);

    最终调到

        private void writeAppWidgets(int[] appWidgetIds) {
            Settings.Secure.putStringForUser(mContentResolver,
                            Settings.Secure.LOCK_SCREEN_APPWIDGET_IDS,
                            combineStrings(appWidgetIds, ","),
                            UserHandle.USER_CURRENT);
        }
    

    也就是说,我们锁屏相关的widget是通过settings存储在数据库里。(具体存储这块内容在研究)


    addWidget流程(绑定一个widget并最终存储到数据库)

    以KeyguardHostView为例,添加一个View到数据库的流程
    在Keyguard中,添加一个widget是通过start一个Settings中的Activity来实现的,上文已经提及KeyguardAppWidgetPickActivity。

    • 当用户点击添加widget的button的时候,会弹出Activity:KeyguardAppWidgetPickActivity,然后Activity会通过PackageManager得到所有的widget并显示出来。
      我们需要关注的是,当某个widget被点击以后发生的事情。
    @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            Item item = mItems.get(position);
            Intent intent = item.getIntent();
    
            int result;
            if (item.extras != null) {
                // If these extras are present it's because this entry is custom.
                // Don't try to bind it, just pass it back to the app.
                result = RESULT_OK;
                setResultData(result, intent);
            } else {
                try {
                    if (mAddingToKeyguard && mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
                        // Found in KeyguardHostView.java
                        final int KEYGUARD_HOST_ID = 0x4B455947;
                        int userId = ActivityManager.getCurrentUser();
                       * mAppWidgetId = AppWidgetHost.allocateAppWidgetIdForPackage(KEYGUARD_HOST_ID,
                                userId, "com.android.keyguard");//重点:关键步骤1
                    }
                    *mAppWidgetManager.bindAppWidgetId(
                            mAppWidgetId, intent.getComponent(), mExtraConfigureOptions);//重点:关键步骤2
                } catch (IllegalArgumentException e) {
                    // This is thrown if they're already bound, or otherwise somehow
                    // bogus.  Set the result to canceled, and exit.  The app *should*
                    // clean up at this point.  We could pass the error along, but
                    // it's not clear that that's useful -- the widget will simply not
                    // appear.
                    result = RESULT_CANCELED;
                }
                setResultData(result, null);
            }
            if (mAddingToKeyguard) {
                onActivityResult(REQUEST_PICK_APPWIDGET, result, mResultData);//重点
            } else {
                finish();
            }
        }
    
    1. allocateAppWidgetIdForPackage最终会调用到AppWidgetServiceImpl的allocateAppWidgetId。
        public int allocateAppWidgetId(String packageName, int hostId) {
            int callingUid = enforceSystemOrCallingUid(packageName);
            Slog.d(TAG, "allocateAppWidgetId uid"+callingUid+" packageName "+packageName);
            synchronized (mAppWidgetIds) {
                if (!mHasFeature) {
                    return -1;
                }
                ensureStateLoadedLocked();
                int appWidgetId = mNextAppWidgetId++;//1.创建一个id
    
                Host host = lookupOrAddHostLocked(callingUid, packageName, hostId);
    
                AppWidgetId id = new AppWidgetId();
                id.appWidgetId = appWidgetId;
                id.host = host;
    
                host.instances.add(id);
                mAppWidgetIds.add(id);2.//添加这个id
    
                saveStateAsync();
                if (DBG) log("Allocating AppWidgetId for " + packageName + " host=" + hostId
                        + " id=" + appWidgetId);
                return appWidgetId;//3.返回创建的id
            }
        }
    

    然后就给这个widget分配了唯一的id。

    2.bindAppWidgetId最终调用到AppWidgetServiceImpl的bindAppWidgetIdImp

        private void bindAppWidgetIdImpl(int appWidgetId, ComponentName provider, Bundle options) {
            if (DBG) log("bindAppWidgetIdImpl appwid=" + appWidgetId
                    + " provider=" + provider);
            final long ident = Binder.clearCallingIdentity();
            try {
                synchronized (mAppWidgetIds) {
                    ...//此处有省略,详见源码
    
                    id.provider = p;
                    if (options == null) {
                        options = new Bundle();
                    }
                    id.options = options;
    
                    // We need to provide a default value for the widget category if it is not specified
                    if (!options.containsKey(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY)) {
                        options.putInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY,
                                AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN);
                    }
    
                    p.instances.add(id);
                    int instancesSize = p.instances.size();
                    if (instancesSize == 1) {
                        // tell the provider that it's ready
                        sendEnableIntentLocked(p);
                    }
    
                    // send an update now -- We need this update now, and just for this appWidgetId.
                    // It's less critical when the next one happens, so when we schedule the next one,
                    // we add updatePeriodMillis to its start time. That time will have some slop,
                    // but that's okay.
                    sendUpdateIntentLocked(p, new int[] { appWidgetId });
    
                    // schedule the future updates
                    registerForBroadcastsLocked(p, getAppWidgetIds(p));
                    saveStateAsync();
                }
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }
    

    这个方法主要就是把刚刚得到的id与这个widget的信息绑定在一起
    注意,当id与widget绑定以后需要调用
    sendUpdateIntentLocked(p, new int[] { appWidgetId })
    来更新这个widget的内容,

        void sendUpdateIntentLocked(Provider p, int[] appWidgetIds) {
            log("sendUpdateIntentLocked");
            if (appWidgetIds != null && appWidgetIds.length > 0) {
                Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
                ///M: added by mtk for debugging @{
                Exception e = new Exception();
                e.printStackTrace();
                /// @}
                intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
                intent.setComponent(p.info.provider);
                mContext.sendBroadcastAsUser(intent, new UserHandle(mUserId));
            }
        }
    

    这个方法主要是发送一个广播,widget的开发者接收到广播以后来更新这个widget的内容。发广播的时间是比widget添加到界面上的时间稍微提前,不过开发者可以在接收到广播以后再更新,这个时间差并不会影响widget接收广播。

    3.当获取了id并且与widget绑定以后,再接着看onItemClick,
    会调用到setResultData(int code, Intent intent),这个方法里有这两句

        mLockPatternUtils.addAppWidget(appWidgetId, 0);//前文已经提到这个方法的作用是把id添加到数据库里。
        finishDelayedAndShowLockScreen(appWidgetId);//重启锁屏
    
    • 至此,把一个widget的加到数据库的流程已经理清,主要有以下几点:
      • mAppWidgetId = AppWidgetHost.allocateAppWidgetIdForPackage//申请id
      • mAppWidgetManager.bindAppWidgetId//绑定id
      • mLockPatternUtils.addAppWidget(appWidgetId, 0);//添加到数据库

    getWidget流程(数据库中的得到widget列表并显示在锁屏)

    同样以KeyguardHostView为起点。
    上文可知,在添加完widget以后会重启锁屏,这个时候会走到KeyguardHostView的onFinishInflate() ——> updateAndAddWidgets();——>addWidgetsFromSettings()

        private void addWidgetsFromSettings() {
            if (mSafeModeEnabled || widgetsDisabled()) {
                addDefaultStatusWidget(0);//添加一些默认的widget
                return;
            }
    
            int insertionIndex = getInsertPageIndex();
    
            // Add user-selected widget
            final int[] widgets = mLockPatternUtils.getAppWidgets();
    
            if (widgets == null) {
                Log.d(TAG, "Problem reading widgets");
            } else {
                for (int i = widgets.length -1; i >= 0; i--) {
                    if (widgets[i] == LockPatternUtils.ID_DEFAULT_STATUS_WIDGET) {
                        addDefaultStatusWidget(insertionIndex);
                    } else {
                        // We add the widgets from left to right, starting after the first page after
                        // the add page. We count down, since the order will be persisted from right
                        // to left, starting after camera.
                        addWidget(widgets[i], insertionIndex, true);
                    }
                }
            }
        }
    
    

    由前文可知,添加widget到数据库的操作是LockPatternUtils.addAppWidget(int widgetId, int index);
    同理,取出数据库中锁屏的widgetid的方法是getAppWidgets
    LockPatternUtils.java

      private int[] getAppWidgets(int userId) {
            String appWidgetIdString = Settings.Secure.getStringForUser(
                    mContentResolver, Settings.Secure.LOCK_SCREEN_APPWIDGET_IDS, userId);
            String delims = ",";
            if (appWidgetIdString != null && appWidgetIdString.length() > 0) {
                String[] appWidgetStringIds = appWidgetIdString.split(delims);
                int[] appWidgetIds = new int[appWidgetStringIds.length];
                for (int i = 0; i < appWidgetStringIds.length; i++) {
                    String appWidget = appWidgetStringIds[i];
                    try {
                        appWidgetIds[i] = Integer.decode(appWidget);
                    } catch (NumberFormatException e) {
                        Log.d(TAG, "Error when parsing widget id " + appWidget);
                        return null;
                    }
                }
                return appWidgetIds;
            }
            return new int[0];
        }
    

    以上,取出了已经添加到锁屏上的widget的列表,继续看KeyguardHostView的addWidget(widgets[i], insertionIndex, true);

        private boolean addWidget(int appId, int pageIndex, boolean updateDbIfFailed) {
            AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appId);//通过取到widget对应的AppWidgetProviderInfo
            if (appWidgetInfo != null) {
                AppWidgetHostView view = mAppWidgetHost.createView(mContext, appId, appWidgetInfo);通过调用AppWidgetHost的CreateView方法将remoteview apply为view再添加到AppWidgetHostView中
                addWidget(view, pageIndex);
                return true;
            } else {
                if (updateDbIfFailed) {//widget信息为空或无效,删除id
                    Log.w(TAG, "*** AppWidgetInfo for app widget id " + appId + "  was null for user"
                            + mUserId + ", deleting");
                    mAppWidgetHost.deleteAppWidgetId(appId);
                    mLockPatternUtils.removeAppWidget(appId);
                }
                return false;
            }
        }
        
    

    AppWidgetHost是通过AppWidgetHostView的view.updateAppWidget(views);方法来获取view的

        public final AppWidgetHostView createView(Context context, int appWidgetId,
                AppWidgetProviderInfo appWidget) {
            Log.d(TAG, "createView appWidgetId "+appWidgetId);
            final int userId = mContext.getUserId();
            AppWidgetHostView view = onCreateView(mContext, appWidgetId, appWidget);
            view.setUserId(userId);
            view.setOnClickHandler(mOnClickHandler);
            view.setAppWidget(appWidgetId, appWidget);
            synchronized (mViews) {
                Log.d(TAG, "createView mViews put "+this);
                mViews.put(appWidgetId, view);
            }
            RemoteViews views;
            try {
                views = sService.getAppWidgetViews(appWidgetId, userId);
                if (views != null) {
                    views.setUser(new UserHandle(mContext.getUserId()));
                }
            } catch (RemoteException e) {
                throw new RuntimeException("system server dead?", e);
            }
            view.updateAppWidget(views);
    
            return view;
        }
        
    

    具体请注意以下两句

    
        public void updateAppWidget(RemoteViews remoteViews) {
    
    ...//省略,详细请查看源码
                // Try normal RemoteView inflation
                if (content == null) {
                    try {
                        /// M: add for using customer view, migrated from GB to ICS to JB
                        remoteViews.setHasUsedCustomerView(mInfo.hasUsedCustomerView);
                        content = remoteViews.apply(mContext, this, mOnClickHandler);
                        if (LOGD) Log.d(TAG, "had to inflate new layout");
                    } catch (RuntimeException e) {
                        exception = e;
                    }
                }
    ...//省略,详细请查看源码
    
            if (mView != content) {
                removeView(mView);
                mView = content;//mView即AppWidgetHostView中添加的View
            }
    ...//省略,详细请查看源码
        }
    

    经过以上步骤,则remoteView被通过id得到且创建为View放进AppWidgetHostView中。

    继续看KeyguardHostView的addWidget(view, pageIndex);

    会调用到mAppWidgetContainer.addWidget(view, pageIndex),mAppWidgetContainer是一个KeyguardWidgetPager的实例,我们可以随便定义一个父View,不一定要放在KeyguardWidgetPager里。

    KeyguardWidgetPager.java

    public void addWidget(View widget, int pageIndex) {
    KeyguardWidgetFrame frame;
    
    ...//省略,详细请看源码
    frame.addView(widget, lp);
    
    ...//省略,详细请看源码
    
    if (pageIndex == -1) {
    addView(frame, pageLp);
    } else {
    addView(frame, pageIndex, pageLp);
    }
    
    ...//省略,详细请看源码
    }
    

    这里只是添加上,还不算完成,
    注意,在KeyguardHostView中的onAttachedToWindow中还有这么一句

    if (!KeyguardViewMediator.isKeyguardInActivity) {
    mAppWidgetHost.startListening();
    }
    

    startListening一定要写,widget才会被定时更新。
    至此,我们的widget被成功添加到锁屏的界面。

    • 主要流程总结如下:
      • widgets = mLockPatternUtils.getAppWidgets();//从数据库中读取
      • appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appId);//通过id取到widget对应的AppWidgetProviderInfo
      • AppWidgetHostView view = mAppWidgetHost.createView(mContext, appId, appWidgetInfo);通过调用AppWidgetHost的CreateView方法将remoteview apply为view再添加到AppWidgetHostView中
      • addWidget(view, pageIndex);//将AppWidgetHostView添加到我们的父View中
      • 注意在view加载完成以后mAppWidgetHost.startListening().

    updatewidget流程(widget的更新)

    上文我们提到两个地方,有关widget的更新

    • 在bindAppWidgetId完成以后,调用 sendUpdateIntentLocked(p, new int[] { appWidgetId });发送广播来更新,这个流程比较清晰,不赘述。
    • 在view加载以后mAppWidgetHost.startListening();来保持View一直会更新

    AppWidgetHost的startListening会调用到AppWidgetService的startListening,AppWidgetService的startListening会返回一个需要更新的widgetid的数组。AppWidgetHost会在得到这个数组以后更新一次这个Host上的所有widget。
    当然,要保持widget持续刷新,更新一次肯定是不够的,所有我们继续看

    第一小步

    AppWidgetService:

    public int[] startListening(IAppWidgetHost callbacks, String packageName, int hostId,
    List<RemoteViews> updatedViews) {
    Slog.d(TAG, "startListening hostId " + hostId);
    。。。//省略
    Host host = lookupOrAddHostLocked(callingUid, packageName, hostId);
    。。。//省略
    return updatedIds;
    }
    }
    

    再来看lookupOrAddHostLocked里做了如下操作

    Host lookupOrAddHostLocked(int uid, String packageName, int hostId) {
    final int N = mHosts.size();
    for (int i = 0; i < N; i++) {
    Host h = mHosts.get(i);
    if (h.hostId == hostId && h.packageName.equals(packageName)) {
    return h;
    }
    }
    Host host = new Host();
    host.packageName = packageName;
    Slog.d(TAG, "lookupOrAddHostLocked h.uid"+uid);
    host.uid = uid;
    host.hostId = hostId;
    mHosts.add(host);//重点
    return host;
    }
    

    这里是把需要更新widget的host加入到AppWidgetService的mHost列表里。

    第二小步

    由于添加widget导致android配置信息改变会进入AppwidgetService的onConfiguartionchanged,这个方法中做了如下操作

    void onConfigurationChanged() {
    if (DBG) log("Got onConfigurationChanged()");
    Locale revised = Locale.getDefault();
    if (revised == null || mLocale == null || !(revised.equals(mLocale))) {
    mLocale = revised;
    
    synchronized (mAppWidgetIds) {
    ensureStateLoadedLocked();
    // Note: updateProvidersForPackageLocked() may remove providers, so we must copy the
    // list of installed providers and skip providers that we don't need to update.
    // Also note that remove the provider does not clear the Provider component data.
    ArrayList<Provider> installedProviders =
    new ArrayList<Provider>(mInstalledProviders);
    HashSet<ComponentName> removedProviders = new HashSet<ComponentName>();
    int N = installedProviders.size();
    for (int i = N - 1; i >= 0; i--) {
    Provider p = installedProviders.get(i);
    ComponentName cn = p.info.provider;
    if (!removedProviders.contains(cn)) {
    updateProvidersForPackageLocked(cn.getPackageName(), removedProviders);//重点1
    }
    }
    saveStateAsync();//重点2
    }
    }
    }
    

    继续看源码(这里不再粘贴)会发现
    updateProvidersForPackageLocked(cn.getPackageName(), removedProviders)
    做了一个操作就是根据配置的更新间隔定时发出更新广播。
    updateProvidersForPackageLocked——>getAppWidgetIds——>sendUpdateIntentLocked(p, appWidgetIds);

    saveStateAsync();会重新更新需要更新的widget的列表的xml。
    saveStateAsync()——>writeStateToFileLocked——>parseProviderInfoXml(component, ri);
    saveStateAsync就把上文startListening更新的mHost更新到xml里,下次定时更新widet的时候就遍历到这里然后把对应的widget进行更新.

    • 至此,widget的更新流程算是走完了,主要有一下两个
      • onconfiguarationchange的时候定时读取xml——>向app发送更希widget的广播——>更新xml,写入需要update的widgetHost的列表。
      • 2.startListening的时候把当前Host的Id通知给AppWidgetService保存在mHost列表里等待下次写进xml。

    小结:

    所以添加一个插件的核心流程为


    流程1.png

    每个部分的大致流程如下:

    添加widget流程

    流程2.png

    showwidget流程

    流程3.png

    更新widget流程

    流程4.png

    相关文章

      网友评论

      本文标题:Android Widget 源码解析——锁屏为例

      本文链接:https://www.haomeiwen.com/subject/szaotttx.html