Android System Slice应用加载分析,及启用Slice后,锁屏日期第一次开机正常显示,但后面锁屏不显示问题分析

最近发现Android P上锁屏界面,日期不显示,发现从P开始后,出现了Slice来允许应用以模块化,可交互的方式,插入多个使用场景。Android P的system UI 也使用到了这一特性,表现为锁屏时间,日期,勿扰图标,闹钟等。

如何使用Slice

以System UI中KeyguardSliceView.java来说明它的使用方法

首先,我们打开System UI 的mk 文件,可以看到以下代码:

LOCAL_STATIC_ANDROID_LIBRARIES := \
    SystemUIPluginLib \
    SystemUISharedLib \
    android-support-car \
    android-support-v4 \
    android-support-v7-recyclerview \
    android-support-v7-preference \
    android-support-v7-appcompat \
    android-support-v7-mediarouter \
    android-support-v7-palette \
    android-support-v14-preference \
    android-support-v17-leanback \
    android-slices-core \
    android-slices-view \
    android-slices-builders \
    android-arch-core-runtime \
    android-arch-lifecycle-extensions \

android-slices-core, view ,builders,就是实现Slice特性的依赖,至于Android studio工程,则需要在gradle中添加以下依赖,

implementation 'androidx.slice:slice-core:1.0.0'
implementation 'androidx.slice:slice-builders:1.0.0'
implementation 'androidx.slice:slice-view:1.0.0'

注册provider

Slice 是一个集合其他模块,共同展示信息的功能,为了实现跨进程间的信息传递,它采用了provider来更新slice,而contentProvider需要在Manifest中注册,如SystemUI:

        <provider android:name=".keyguard.KeyguardSliceProvider"
                  android:authorities="com.android.systemui.keyguard"
                  android:grantUriPermissions="true"
                  android:exported="true">
        </provider>

通过查看KeyguardSliceProvider.java,可以看到它继承了 SliceProvider,而SliceProvider又继承了ContentProvider。这里关注两个函数onCreateSliceProvider()、onBindSlice()。onCreateSliceProvider()是SliceProvider初始化的时候调用的:

    @Override
    public final boolean onCreate() {
        if (!BuildCompat.isAtLeastP()) {
            mCompat = new SliceProviderCompat(this,
                    onCreatePermissionManager(mAutoGrantPermissions), getContext());
        }
        return onCreateSliceProvider();
    }

o’nCreateSliceProvider中做了如下的操作:

    @Override
    public boolean onCreateSliceProvider() {
        mAlarmManager = getContext().getSystemService(AlarmManager.class);
        mContentResolver = getContext().getContentResolver();
        mNextAlarmController = new NextAlarmControllerImpl(getContext()); 
        mNextAlarmController.addCallback(this);
        mZenModeController = new ZenModeControllerImpl(getContext(), mHandler);
        mZenModeController.addCallback(this);
        mDatePattern = getContext().getString(R.string.system_ui_aod_date_pattern);
        mPendingIntent = PendingIntent.getActivity(getContext(), 0, new Intent(), 0);
        KeyguardSliceProvider.sInstance = this;
        registerClockUpdate();
        updateClock();
        return true;
    }

主要是对几个需要显示的模块的监听初始化,包括Alarm,ZenMode,Clock;
注册之后是怎么进行刷新的呢?既然是provider,那么肯定有地方进行notifyChange的,通过对该类的阅读,发现在注册的一些callback中就有notifyChange,如下面的ZenMode:

public class KeyguardSliceProvider extends SliceProvider implements
        NextAlarmController.NextAlarmChangeCallback, ZenModeController.Callback 

复写了ZenMode的onZenChanged,

    @Override
    public void onZenChanged(int zen) {
        mContentResolver.notifyChange(mSliceUri, null /* observer */);
    }

那么在notifyChange之后,接下来就会进行刚刚说到的onBindSlice():

    @Override
    public Slice onBindSlice(Uri sliceUri) {
        Trace.beginSection("KeyguardSliceProvider#onBindSlice");
        ListBuilder builder = new ListBuilder(getContext(), mSliceUri);
        builder.addRow(new RowBuilder(builder, mDateUri).setTitle(mLastText));
        addNextAlarm(builder);
        addZenMode(builder);
        addPrimaryAction(builder);
        Slice slice = builder.build();
        Trace.endSection();
        return slice;
    }

这边和notification的创建类似,就是各个模块传递过来的icon(Row),title之类的信息。

KeyguardSliceView

SystemUi从KeyguardSliceProvider获取数据,那么如何刷新界面呢,其主要操作就是在KeyguardSliceView这里。
在KeyguardStatusView.java中:

mKeyguardSlice = findViewById(R.id.keyguard_status_area);
mKeyguardSlice.setContentChangeListener(this::onSliceContentChanged);

接下来看keyguard_status_area的布局:

<com.android.keyguard.KeyguardSliceView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_marginStart="16dp"
    android:layout_marginEnd="16dp"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:clipToPadding="false"
    android:orientation="vertical"
    android:layout_centerHorizontal="true">
    <view class="com.android.keyguard.KeyguardSliceView$TitleView"
              android:id="@+id/title"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:layout_marginBottom="@dimen/widget_title_bottom_margin"
              android:paddingStart="64dp"
              android:paddingEnd="64dp"
              android:visibility="gone"
              android:textColor="?attr/wallpaperTextColor"
              android:theme="@style/TextAppearance.Keyguard"
    />
    <view class="com.android.keyguard.KeyguardSliceView$Row"
              android:id="@+id/row"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:orientation="horizontal"
              android:gravity="center"
    />
</com.android.keyguard.KeyguardSliceView>

可以看到其包含了两个自定义控件,着重关注Row控件,日期,闹钟,勿扰等图标都是在Row中的。

Slice 监听

Slice是如何监听,并且刷新到UI的呢,我们从KeyguardSliceView的初始化看起,可以看到,它实现了 TunerService.Tunable接口,所以初始化的时候就会调用onTuningChanged:

    @Override
    public void onTuningChanged(String key, String newValue) {
        setupUri(newValue);
    }

    public void setupUri(String uriString) {
        if (uriString == null) {
            uriString = KeyguardSliceProvider.KEYGUARD_SLICE_URI;
        }

        boolean wasObserving = false;
        if (mLiveData != null && mLiveData.hasActiveObservers()) {
            wasObserving = true;
            mLiveData.removeObserver(this);
        }

        mKeyguardSliceUri = Uri.parse(uriString);
        mLiveData = SliceLiveData.fromUri(mContext, mKeyguardSliceUri);

        if (wasObserving) {
            mLiveData.observeForever(this);
        }
    }

这里要重点注意,和bug有关,uriString 在KeyguardSliceView初始化时,默认是null,但会立刻被填充 KeyguardSliceProvider.KEYGUARD_SLICE_URI,随后,会被保存到mLiveData中。

mLiveData

private LiveData mLiveData;
可以看到mLiveData是一个slice 集合类,Android 给它的解释是LiveData is a data holder class that can be observed within a given lifecycle.,意思是LiveData是一个可被观察的数据持有类,不同于其他Observer,LiveData是对生命周期有感知的,它会遵循App组件的生命周期,如Activity,Fragment,Service等。
它的优点在于:

  1. 实时同步UI和数据
    LiveData采用了观察者模式设计,其中LiveData是被观察者,当数据发生变化时会通知观察者进行数据更新。通过这点,可以确保数据和界面的实时性。
  2. 有效规避内存泄露
    这是因为LiveData能够感知到组件的生命周期,当组件状态处于DESTROYED状态时,观察者对象会被remove。
  3. 不会因为组件销毁导致崩溃
    这是因为组件处于非激活状态时,在界面不会收到来自LiveData的数据变化通知。这样规避了很多因为页面销毁之后,修改UI导致的crash。
  4. 无需手动处理生命周期
    LiveData能够感知组件的生命周期,无需设置LiveData组件的生命周期状态。
  5. 始终保持数据更新
    生命周期从非活跃状态切换到活跃状态的时候,能够实时的接收最新的数据。
  6. 不会因为配置变化导致数据丢失(onConfigChanged)
    由于LiveData保存数据的时候,组件和数据是分离的,所以在配置更改(如横竖屏切换等)的时候,即便组件被重新创建,因为数据还保存在LiveData中,这样也能够做到实时的更新。
  7. 资源共享
    单例模式扩展LiveData对象并包装成系统服务,以便在应用程序中进行共享,需要该资源的只需要观察LiveData即可。

它的注册和反注册:

     *
     * @param observer The observer that will receive the events
     */
    @MainThread
    public void observeForever(@NonNull Observer<? super T> observer) {
        assertMainThread("observeForever");
        AlwaysActiveObserver wrapper = new AlwaysActiveObserver(observer);
        ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
        if (existing != null && existing instanceof LiveData.LifecycleBoundObserver) {
            throw new IllegalArgumentException("Cannot add the same observer"
                    + " with different lifecycles");
        }
        if (existing != null) {
            return;
        }
        wrapper.activeStateChanged(true);
    }

    /**
     * Removes the given observer from the observers list.
     *
     * @param observer The Observer to receive events.
     */
    @MainThread
    public void removeObserver(@NonNull final Observer<? super T> observer) {
        assertMainThread("removeObserver");
        ObserverWrapper removed = mObservers.remove(observer);
        if (removed == null) {
            return;
        }
        removed.detachObserver();
        removed.activeStateChanged(false);
    }

上面提到,Livedata感知生命周期,因此它的注册与反注册需要跟着观察者的生命周期走,如KeyguardSliceView中,它的注册与反注册,就是在onAttachedToWindow() 和onDetachedFromWindow() 中处理的,如下:

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();

        // Make sure we always have the most current slice
        mLiveData.observeForever(this);
        Dependency.get(ConfigurationController.class).addCallback(this);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();

        mLiveData.removeObserver(this);
        Dependency.get(ConfigurationController.class).removeCallback(this);
    }

因KeyguardSliceView实现了Observer,所以一旦slice控件发生变化,就会回调到onChange(),从而触发刷新页面。


    /**
     * LiveData observer lifecycle.
     * @param slice the new slice content.
     */
    @Override
    public void onChanged(Slice slice) {
        mSlice = slice;
        showSlice();
    }

最后看showSlice()的代码:


    private void showSlice() {
        Trace.beginSection("KeyguardSliceView#showSlice");
        if (mPulsing || mSlice == null) {
            mTitle.setVisibility(GONE);
            mRow.setVisibility(GONE);
            if (mContentChangeListener != null) {
                mContentChangeListener.run();
            }
            return;
        }

        ListContent lc = new ListContent(getContext(), mSlice);
        mHasHeader = lc.hasHeader();
        List<SliceItem> subItems = new ArrayList<SliceItem>();
        for (int i = 0; i < lc.getRowItems().size(); i++) {                                 //逐条解析mSlice中的slice
            SliceItem subItem = lc.getRowItems().get(i);                 
            String itemUri = subItem.getSlice().getUri().toString();
            // Filter out the action row
            if (!KeyguardSliceProvider.KEYGUARD_ACTION_URI.equals(itemUri)) {
                subItems.add(subItem);                                                  //如果KeyguardSliceProvider中存在相应的Uri,上面有提及初始化的情况,个人理解就是需要显示slice控件的app,需要在provider中注册
            }
        }
        if (!mHasHeader) {
            mTitle.setVisibility(GONE);
        } else {
            mTitle.setVisibility(VISIBLE);

            // If there's a header it'll be the first subitem
            RowContent header = new RowContent(getContext(), subItems.get(0),
                    true /* showStartItem */);
            SliceItem mainTitle = header.getTitleItem();
            CharSequence title = mainTitle != null ? mainTitle.getText() : null;
            mTitle.setText(title);
        }

        mClickActions.clear();
        final int subItemsCount = subItems.size();
        final int blendedColor = getTextColor();
        final int startIndex = mHasHeader ? 1 : 0; // First item is header; skip it
        mRow.setVisibility(subItemsCount > 0 ? VISIBLE : GONE);
        for (int i = startIndex; i < subItemsCount; i++) {
            SliceItem item = subItems.get(i);
            RowContent rc = new RowContent(getContext(), item, true /* showStartItem */);   
            final Uri itemTag = item.getSlice().getUri();
            // Try to reuse the view if already exists in the layout
            KeyguardSliceButton button = mRow.findViewWithTag(itemTag);               //将获取的URi解析后,逐条填充到KeyguardSliceButton,这也是一个自定义控件
            if (button == null) {
                button = new KeyguardSliceButton(mContext);
                button.setTextColor(blendedColor);
                button.setTag(itemTag);
                final int viewIndex = i - (mHasHeader ? 1 : 0);
                mRow.addView(button, viewIndex);
            }

            PendingIntent pendingIntent = null; 
            if (rc.getPrimaryAction() != null) { 
                pendingIntent = rc.getPrimaryAction().getAction();             //可跳转操作
            }
            mClickActions.put(button, pendingIntent);

            final SliceItem titleItem = rc.getTitleItem();
            button.setText(titleItem == null ? null : titleItem.getText());             
            button.setContentDescription(rc.getContentDescription());       

            Drawable iconDrawable = null;
            SliceItem icon = SliceQuery.find(item.getSlice(),
                    android.app.slice.SliceItem.FORMAT_IMAGE);
            if (icon != null) {
                iconDrawable = icon.getIcon().loadDrawable(mContext);
                final int width = (int) (iconDrawable.getIntrinsicWidth()              
                        / (float) iconDrawable.getIntrinsicHeight() * mIconSize);
                iconDrawable.setBounds(0, 0, Math.max(width, 1), mIconSize);       
            }
            button.setCompoundDrawables(iconDrawable, null, null, null);                  //这里填充slice icon,就是我们见到的显示的slice 图标,如闹钟。
            button.setOnClickListener(this);
            button.setClickable(pendingIntent != null);
        }

        // Removing old views
        for (int i = 0; i < mRow.getChildCount(); i++) {
            View child = mRow.getChildAt(i);
            if (!mClickActions.containsKey(child)) {
                mRow.removeView(child);
                i--;
            }
        }

        if (mContentChangeListener != null) {
            mContentChangeListener.run();
        }
        Trace.endSection();
    }

整个流程分析结束,现在回过头来看bug:为什么锁屏上的date会消失呢。

看上方showSlice()代码,mRow.setVisibility(subItemsCount > 0 ? VISIBLE : GONE);,我们在前文看到,mRow实际上就是显示日期,闹钟,勿扰图标的,那么在不显示的情况下,可以确定subItemsCount = 0的,那么往上追,它的count值,是由onChange时,获取到的mLiveDate的条目数决定的,而liveDate中的slice,是由StringUri解析而来:

        mKeyguardSliceUri = Uri.parse(uriString);
        mLiveData = SliceLiveData.fromUri(mContext, mKeyguardSliceUri);

因此,可以确定,在某种情况下, mKeyguardSliceUri 解析出了问题,此时,我们打印出 mKeyguardSliceUri 的值,发现值为:

content://com.android.systemui.keyguard/main

这个值,在KeyguardSliceProvider.java中,定义为:


 public static final String KEYGUARD_SLICE_URI = "content://com.android.systemui.keyguard/main";

这时,我们发现,KEYGUARD_SLICE_URI正是在setupUri时,默认填充了,所以Slice组件就认为此处无任何row图标,因此只要默认填充date的URI,就可以发现,每次date都是默认填充的了,查看KeyguardSliceProvider.java,它的值是:

public static final String KEYGUARD_DATE_URI = "content://com.android.systemui.keyguard/date";

因此,代码修改为:

    public void setupUri(String uriString) {
        if (uriString == null) {
            //uriString = KeyguardSliceProvider.KEYGUARD_SLICE_URI;
             uriString = KeyguardSliceProvider.KEYGUARD_DATE_URI ;
        }

        boolean wasObserving = false;
        if (mLiveData != null && mLiveData.hasActiveObservers()) {
            wasObserving = true;
            mLiveData.removeObserver(this);
        }

        mKeyguardSliceUri = Uri.parse(uriString);
        mLiveData = SliceLiveData.fromUri(mContext, mKeyguardSliceUri);

        if (wasObserving) {
            mLiveData.observeForever(this);
        }
    }

编译后,问题解决。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值