Android常見問題總結(三)

1.Https是如何實現的

htttps是基於ssl(Secure Sockets Layer安全套接層)的http協議,https協議在http協議與tcp協議之間增加一層安全層,數據在網絡傳輸之前,會先進行加密,再進行傳輸。

在這裏插入圖片描述
https的主要流程如下:
在這裏插入圖片描述
2.Android事件流程和OnTouchListener的關係
3.雙指縮放拖動大圖

4.客戶端網絡安全實現
1.使用https
2.設置網絡安全配置文件

5.Android應用保活
1.單進程守護、雙進程守護
原理:利用其它app與自身綁定Service
2.開啓前臺進程的Service,監聽系統鎖屏廣播製造“1”像素的懸浮窗
3.循環在後臺播放一段無聲音樂

6.RemoteViews的實現和使用場景
RemoteViews 的作用是在其他進程中顯示並更新 View 界面。主要用於通知欄和桌面小部件上。

1 通知欄的使用
我們使用 NotificationCompat.Builder.build() 來創建一個通知,然後調用 NotificationManager.notify() 來顯示通知欄,在需要自定義通知欄 UI 時,就需要 RemoteViews 來幫忙了。

第一步:設置通知欄的 UI 佈局文件
第二步:使用 RemoteViews 綁定佈局
注意:Android 8.0 (API 26) 以上的手機需要 NotificationChannel 實現通知欄。

@TargetApi(26)
void testRemoteViewsInNotification() {
    Intent intent = new Intent(this, NotiActivity.class);
    PendingIntent pendingIntent = PendingIntent
            .getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

    String id = "my_channel_01";
    CharSequence name = "channel";
    String description = "description";
    int importance = NotificationManager.IMPORTANCE_DEFAULT;
    NotificationChannel channel = new NotificationChannel(id, name, importance);
    channel.setDescription(description);
    channel.enableLights(true);
    channel.setLightColor(Color.RED);
    channel.enableVibration(true);
    // 偶數表示靜止時間,奇數表示振動時間
    channel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400});

    NotificationManager manager =
            (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    manager.createNotificationChannel(channel);

    RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_notification);
    remoteViews.setTextViewText(R.id.msg, "demo");
    remoteViews.setOnClickPendingIntent(R.id.notification, pendingIntent);

    Notification notificaton = new Notification.Builder(this, id)
            .setAutoCancel(false)
            .setContentTitle("title")
            .setContentText("text")
            .setSmallIcon(R.mipmap.ic_launcher_round)
            .setOngoing(true)
            .setCustomContentView(remoteViews)
            .setWhen(System.currentTimeMillis())
            .build();
    manager.notify(1, notificaton);
}

2 桌面小部件的使用
第一步:定義小部件界面

<!-- res/layout/my_app_widget.xml -->
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorAccent"
    android:padding="8dp">

    <TextView
        android:id="@+id/my_app_widget_view_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:layout_margin="8dp"
        android:text="@string/app_name"
        android:textSize="24sp"
        android:textStyle="bold|italic"/>

</RelativeLayout>

第二步:定義小部件配置信息

<!-- res/xml/my_app_widget_info.xml -->
<appwidget-provider
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/my_app_widget"
    android:minHeight="84dp"
    android:minWidth="84dp"
    android:updatePeriodMillis="86400000">

</appwidget-provider>

第三步:定義小部件的實現類

public class MyAppWidgetProvider extends AppWidgetProvider {
    public static final String CLICK_ACTION = "com.mindle.androidtest.action.CLICK";

    static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
                                int appWidgetId) {
        // Construct the RemoteViews object
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.my_app_widget);

        Intent onIntent = new Intent();
        onIntent.setAction(CLICK_ACTION);
        PendingIntent onPendingIntent = PendingIntent.getBroadcast(context, 0, onIntent, 0);
        remoteViews.setOnClickPendingIntent(R.id.my_app_widget_view_text, onPendingIntent);

        // Instruct the widget manager to update the widget
        appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
    }

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // There may be multiple widgets active, so update all of them
        for (int appWidgetId : appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    }

    @Override
    public void onEnabled(Context context) {
        // Enter relevant functionality for when the first widget is created
    }

    @Override
    public void onDisabled(Context context) {
        // Enter relevant functionality for when the last widget is disabled
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        super.onReceive(context, intent);
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.my_app_widget);
        if (Objects.equals(intent.getAction(), CLICK_ACTION)) {
            Toast.makeText(context, "hello, widget!", Toast.LENGTH_SHORT).show();

            AppWidgetManager manager = AppWidgetManager.getInstance(context);

            ComponentName thisName = new ComponentName(context, MyAppWidgetProvider.class);

            manager.updateAppWidget(thisName, remoteViews);
        }
    }
}

最後一步:註冊桌面小部件

<receiver android:name=".NewAppWidget">
    <intent-filter>
        <action android:name="com.mindle.androidtest.action.CLICK"/>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
    </intent-filter>

    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/new_app_widget_info"/>
</receiver>

工作原理
RemoteViews 是實現了 Parcelable 接口的,可以通過 Binder 傳遞到其他進程中。比如,前面提到的兩個使用場景中,通知欄和桌面小部件分別由 NotificationManager 和 AppWidgetManager 管理,它們 通過 Binder 分別和 SystemServer 進程中的 NotificationManagerService 以及 AppWidgetManagerService 通信,實現了跨進程傳遞 RemoteViews。

RemoteViews 的一系列 set 操作是通過將更新操作包裝成 Action(Parcelable)對象集合保存下來。Action 最重要的是 reply 方法,通過反射或別的方法,在其它進程中更新 RemoteViews 中控件的佈局。

RemoteViews 中有 apply 方法和 reApply 方法。apply 方法會加載佈局並更新界面,而 reApply 方法僅僅更新界面。通知欄和桌面小部件在初始化界面時會調用 apply 方法,在後續的更新界面時會調用 reApply 方法。

注意:RemoteViews 的 apply 方法會加載佈局,如下面代碼所示,其中 rv.getLayoutId() 得到的 RemoteViews 的佈局文件 ID 是在構造函數中傳進去的,即 RemoteViews(packageName, layoutId)。因爲 RemoteViews 常用於在其他進程中顯示並更新 View 界面,但是如果兩個進程屬於不同的應用,那麼它們的資源 ID 很可能是不一致的。在應用 A 中使用資源 ID1 創建了 RemoteViews 傳給應用 B 之後,apply 方法中直接在應用 B 中用 ID1 來繪製佈局很可能會出錯。

解決上面問題的辦法是,手動加載佈局,然後用 reApply 方法來更新界面。如下面的例子所示,通過約定好的佈局文件名來獲取資源 ID,然後再手動加載佈局。

int layoutId = getResources()
        .getIdentifier("layout_notification", "layout", getPackageName());
View v = getLayoutInflater().inflate(layoutId, layout, false);
remoteViews.reapply(this, v);
layout.addView(v);

7.RecyclerView的繪製步驟和複用機制

繪製流程:
onMeasure:

protected void onMeasure(int widthSpec, int heightSpec) {
    /** 1.判斷mLayout是否爲null,爲null執行defaultOnMeasure,return **/
    if (mLayout == null) {
        defaultOnMeasure(widthSpec, heightSpec);
        return;
    }
    /** 2.isAutoMeasureEnabled()一般都是true **/
    if (mLayout.isAutoMeasureEnabled()) {
        final int widthMode = MeasureSpec.getMode(widthSpec);
        final int heightMode = MeasureSpec.getMode(heightSpec);
        /** 3.調用了LayoutManager的onMeasure **/
        mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
        /** 4.當RecyclerView的寬高都是EXACTLY時,skipMeasure爲true,那麼接下來直接return **/
        final boolean measureSpecModeIsExactly =
                widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
        if (measureSpecModeIsExactly || mAdapter == null) {
            return;
        }
        /** 5.mState.mLayoutStep爲STEP_START,這裏會執行dispatchLayoutStep1(); **/
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
        }
        // set dimensions in 2nd step. Pre-layout should happen with old dimensions for
        // consistency
        mLayout.setMeasureSpecs(widthSpec, heightSpec);
        mState.mIsMeasuring = true;
        /** 6.執行dispatchLayoutStep2();這個方法很重要,下面繼續分析 **/
        dispatchLayoutStep2();
        // now we can get the width and height from the children.
        mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
        // if RecyclerView has non-exact width and height and if there is at least one child
        // which also has non-exact width & height, we have to re-measure.
        if (mLayout.shouldMeasureTwice()) {
            mLayout.setMeasureSpecs(
                    MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
            mState.mIsMeasuring = true;
            dispatchLayoutStep2();
            // now we can get the width and height from the children.
            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
        }
    } else {
        if (mHasFixedSize) {
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            return;
        }
        // custom onMeasure
        if (mAdapterUpdateDuringMeasure) {
            startInterceptRequestLayout();
            onEnterLayoutOrScroll();
            processAdapterUpdatesAndSetAnimationFlags();
            onExitLayoutOrScroll();
            if (mState.mRunPredictiveAnimations) {
                mState.mInPreLayout = true;
            } else {
                // consume remaining updates to provide a consistent state with the layout pass.
                mAdapterHelper.consumeUpdatesInOnePass();
                mState.mInPreLayout = false;
            }
            mAdapterUpdateDuringMeasure = false;
            stopInterceptRequestLayout(false);
        } else if (mState.mRunPredictiveAnimations) {
            // If mAdapterUpdateDuringMeasure is false and mRunPredictiveAnimations is true:
            // this means there is already an onMeasure() call performed to handle the pending
            // adapter change, two onMeasure() calls can happen if RV is a child of LinearLayout
            // with layout_width=MATCH_PARENT. RV cannot call LM.onMeasure() second time
            // because getViewForPosition() will crash when LM uses a child to measure.
            setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight());
            return;
        }
        if (mAdapter != null) {
            mState.mItemCount = mAdapter.getItemCount();
        } else {
            mState.mItemCount = 0;
        }
        startInterceptRequestLayout();
        mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
        stopInterceptRequestLayout(false);
        mState.mInPreLayout = false; // clear
    }
}

1.mLayout爲null時,只是繪製了RecyclerView的寬高,這也就是不設置setLayoutManager導致RecyclerView顯示爲空白的原因。
2.isAutoMeasureEnabled() 是否爲自動測量,返回爲true,則執行RecyclerView的autoMeasure,否則執行layoutManager的onMeasure。LinearLayoutManager的構造方法裏自動設置爲true。
3.onMeasure主要調用了dispatchLayoutStep1和dispatchLayoutStep2,onLayout會調用dispatchLayoutStep3
dispatchLayoutStep1: Adapter的更新; 決定該啓動哪種動畫; 保存當前View的信息(getLeft(), getRight(), getTop(), getBottom()等); 如果有必要,先跑一次佈局並將信息保存下來。
dispatchLayoutStep2:真正對子View做佈局的地方。
dispatchLayoutStep3:爲動畫保存View的相關信息; 觸發動畫; 相應的清理工作。

RecyclerView 滑動場景下的回收複用涉及到的結構體兩個:mCachedViews 和 RecyclerViewPool。

mCachedViews 優先級高於 RecyclerViewPool,回收時,最新的 ViewHolder 都是往 mCachedViews 裏放,如果它滿了,那就移出一個扔到 ViewPool 裏好空出位置來緩存最新的 ViewHolder。

複用時,也是先到 mCachedViews 裏找 ViewHolder,但需要各種匹配條件,概括一下就是隻有原來位置的卡位可以複用存在 mCachedViews 裏的 ViewHolder,如果 mCachedViews 裏沒有,那麼纔去 ViewPool 裏找。

在 ViewPool 裏的 ViewHolder 都是跟全新的 ViewHolder 一樣,只要 type 一樣,有找到,就可以拿出來複用,重新綁定下數據即可。

複用的流程圖如下:
在這裏插入圖片描述
8.Finalize機制:
finalize是Object下的方法,任何類都可以重寫,jdk9以後使用cleaner機制代替了finalize機制。

參考博客:
RemoteViews的原理及使用場景:https://blog.csdn.net/weixin_40255793/article/details/81482266
RecyclerView的繪製流程:https://www.jianshu.com/p/f91b41c8f487
RecyclerView的複用機制:https://www.cnblogs.com/dasusu/p/7746946.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章