主頁 > 移動端開發 > Android學習指南 — Android基礎知識匯總

Android學習指南 — Android基礎知識匯總

2021-10-21 08:14:27 移動端開發

本次的學習指南系列將分為三個模塊

  • 第一 Java知識點匯總
  • 第二 Android基礎知識點匯總(一)(二)
  • 第三 Android進階知識點匯總

后續的NDK、跨平臺、底層原始碼等技術,也會抽空給大家整理一下知識點,如果喜歡的話,希望大家給個關注,點個贊唄,個人主頁簡介有聯系方式,可找我拿PDF版本的學習指南,

怎么聯系我:Android.md · master · 讓開,我要吃人了 / Android · CODE CHINACODE CHINA——開源代碼托管平臺,獨立第三方開源社區,Git/Github/Gitlabhttps://codechina.csdn.net/weixin_55362248/android/-/blob/master/Android.md

Activity

生命周期

  • Activity A 啟動另一個Activity B,回呼如下:
    Activity A 的onPause() → Activity B的onCreate() → onStart() → onResume() → Activity A的onStop();如果B是透明主題又或則是個DialogActivity,則不會回呼A的onStop;
  • 使用onSaveInstanceState()保存簡單,輕量級的UI狀態
lateinit var textView: TextView
var gameState: String? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    gameState = savedInstanceState?.getString(GAME_STATE_KEY)
    setContentView(R.layout.activity_main)
    textView = findViewById(R.id.text_view)
}

override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
    textView.text = savedInstanceState?.getString(TEXT_VIEW_KEY)
}

override fun onSaveInstanceState(outState: Bundle?) {
    outState?.run {
        putString(GAME_STATE_KEY, gameState)
        putString(TEXT_VIEW_KEY, textView.text.toString())
    }
    super.onSaveInstanceState(outState)
}

啟動模式

LaunchMode說明
standard系統在啟動它的任務中創建 activity 的新實體
singleTop如果activity的實體已存在于當前任務的頂部,則系統通過呼叫其onNewIntent(),否則會創建新實體
singleTask系統創建新 task 并在 task 的根目錄下實體化 activity,但如果 activity 的實體已存在于單獨的任務中,則呼叫其 onNewIntent() 方法,其上面的實體會被移除堆疊,一次只能存在一個 activity 實體
singleInstance相同 singleTask,activity始終是其task的唯一成員; 任何由此開始的activity 都在一個單獨的 task 中打開

啟動程序

ActivityThread.java

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ...
    ActivityInfo aInfo = r.activityInfo;
    if (r.packageInfo == null) {
        //step 1: 創建LoadedApk物件
        r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
                Context.CONTEXT_INCLUDE_CODE);
    }
    ... //component初始化程序

    java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
    //step 2: 創建Activity物件
    Activity activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
    ...

    //step 3: 創建Application物件
    Application app = r.packageInfo.makeApplication(false, mInstrumentation);

    if (activity != null) {
        //step 4: 創建ContextImpl物件
        Context appContext = createBaseContextForActivity(r, activity);
        CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
        Configuration config = new Configuration(mCompatConfiguration);
        //step5: 將Application/ContextImpl都attach到Activity物件
        activity.attach(appContext, this, getInstrumentation(), r.token,
                r.ident, app, r.intent, r.activityInfo, title, r.parent,
                r.embeddedID, r.lastNonConfigurationInstances, config,
                r.referrer, r.voiceInteractor);

        ...
        int theme = r.activityInfo.getThemeResource();
        if (theme != 0) {
            activity.setTheme(theme);
        }

        activity.mCalled = false;
        if (r.isPersistable()) {
            //step 6: 執行回呼onCreate
            mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
        } else {
            mInstrumentation.callActivityOnCreate(activity, r.state);
        }

        r.activity = activity;
        r.stopped = true;
        if (!r.activity.mFinished) {
            activity.performStart(); //執行回呼onStart
            r.stopped = false;
        }
        if (!r.activity.mFinished) {
            //執行回呼onRestoreInstanceState
            if (r.isPersistable()) {
                if (r.state != null || r.persistentState != null) {
                    mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state,
                            r.persistentState);
                }
            } else if (r.state != null) {
                mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);
            }
        }
        ...
        r.paused = true;
        mActivities.put(r.token, r);
    }

    return activity;
}

Fragment

特點

  • Fragment 解決 Activity 間的切換不流暢,輕量切換
  • 可以從 startActivityForResult 中接收到回傳結果,但是View不能
  • 只能在 Activity 保存其狀態(用戶離開 Activity)之前使用 commit() 提交事務,如果您試圖在該時間點后提交,則會引發例外, 這是因為如需恢復 Activity,則提交后的狀態可能會丟失, 對于丟失提交無關緊要的情況,請使用 commitAllowingStateLoss(),

生命周期

與Activity通信

執行此操作的一個好方法是,在片段內定義一個回呼介面,并要求宿主 Activity 實作它,

public static class FragmentA extends ListFragment {
    ...
    // Container Activity must implement this interface
    public interface OnArticleSelectedListener {
        public void onArticleSelected(Uri articleUri);
    }
    ...
}

public static class FragmentA extends ListFragment {
    OnArticleSelectedListener mListener;
    ...
    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            mListener = (OnArticleSelectedListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString());
        }
    }
    ...
}

Service

Service 分為兩種作業狀態,一種是啟動狀態,主要用于執行后臺計算;另一種是系結狀態,主要用于其他組件和 Service 的互動,

啟動程序

ActivityThread.java

@UnsupportedAppUsage
private void handleCreateService(CreateServiceData data) {
    ···
    LoadedApk packageInfo = getPackageInfoNoCheck(
            data.info.applicationInfo, data.compatInfo);
    Service service = null;
    try {
        java.lang.ClassLoader cl = packageInfo.getClassLoader();
        service = packageInfo.getAppFactory()
                .instantiateService(cl, data.info.name, data.intent);
    } 
    ···

    try {
        if (localLOGV) Slog.v(TAG, "Creating service " + data.info.name);

        ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
        context.setOuterContext(service);

        Application app = packageInfo.makeApplication(false, mInstrumentation);
        service.attach(context, this, data.info.name, data.token, app,
                ActivityManager.getService());
        service.onCreate();
        mServices.put(data.token, service);
        try {
            ActivityManager.getService().serviceDoneExecuting(
                    data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    } 
    ··· 
}

系結程序

ActivityThread.java

private void handleBindService(BindServiceData data) {
    Service s = mServices.get(data.token);
    ···
    if (s != null) {
        try {
            data.intent.setExtrasClassLoader(s.getClassLoader());
            data.intent.prepareToEnterProcess();
            try {
                if (!data.rebind) {
                    IBinder binder = s.onBind(data.intent);
                    ActivityManager.getService().publishService(
                            data.token, data.intent, binder);
                } else {
                    s.onRebind(data.intent);
                    ActivityManager.getService().serviceDoneExecuting(
                            data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
                }
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
        } 
        ···
    }
}

生命周期

說明
START_NOT_STICKY如果系統在 onStartCommand() 回傳后終止服務,則除非有掛起 Intent 要傳遞,否則系統不會重建服務,這是最安全的選項,可以避免在不必要時以及應用能夠輕松重啟所有未完成的作業時運行服務
START_STICKY如果系統在 onStartCommand() 回傳后終止服務,則會重建服務并呼叫 onStartCommand(),但不會重新傳遞最后一個 Intent,相反,除非有掛起 Intent 要啟動服務(在這種情況下,將傳遞這些 Intent ),否則系統會通過空 Intent 呼叫 onStartCommand(),這適用于不執行命令、但無限期運行并等待作業的媒體播放器(或類似服務
START_REDELIVER_INTENT如果系統在 onStartCommand() 回傳后終止服務,則會重建服務,并通過傳遞給服務的最后一個 Intent 呼叫 onStartCommand(),任何掛起 Intent 均依次傳遞,這適用于主動執行應該立即恢復的作業(例如下載檔案)的服務

啟用前臺服務

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
Notification notification = new Notification(icon, text, System.currentTimeMillis());
Intent notificationIntent = new Intent(this, ExampleActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
notification.setLatestEventInfo(this, title, mmessage, pendingIntent);
startForeground(ONGOING_NOTIFICATION_ID, notification);

BroadcastReceiver

target 26 之后,無法在 AndroidManifest 顯示宣告大部分廣播,除了一部分必要的廣播,如:

  • ACTION_BOOT_COMPLETED
  • ACTION_TIME_SET
  • ACTION_LOCALE_CHANGED
LocalBroadcastManager.getInstance(MainActivity.this).registerReceiver(receiver, filter);

注冊程序

ContentProvider

ContentProvider 管理對結構化資料集的訪問,它們封裝資料,并提供用于定義資料安全性的機制, 內容提供程式是連接一個行程中的資料與另一個行程中運行的代碼的標準界面,

ContentProvider 無法被用戶感知,對于一個 ContentProvider 組件來說,它的內部需要實作增刪該查這四種操作,它的內部維持著一份資料集合,這個資料集合既可以是資料庫實作,也可以是其他任何型別,如 List 和 Map,內部的 insert、delete、update、query 方法需要處理好執行緒同步,因為這幾個方法是在 Binder 執行緒池中被呼叫的,

ContentProvider 通過 Binder 向其他組件乃至其他應用提供資料,當 ContentProvider 所在的行程啟動時,ContentProvider 會同時啟動并發布到 AMS 中,需要注意的是,這個時候 ContentProvider 的 onCreate 要先于 Application 的 onCreate 而執行,

基本使用

// Queries the user dictionary and returns results
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,   // The content URI of the words table
    mProjection,                        // The columns to return for each row
    mSelectionClause                    // Selection criteria
    mSelectionArgs,                     // Selection criteria
    mSortOrder);                        // The sort order for the returned rows
public class Installer extends ContentProvider {

    @Override
    public boolean onCreate() {
        return true;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        return null;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        return null;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }
}

ContentProvider 和 sql 在實作上有什么區別?

  • ContentProvider 屏蔽了資料存盤的細節,內部實作透明化,用戶只需關心 uri 即可(是否匹配)
  • ContentProvider 能實作不同 app 的資料共享,sql 只能是自己程式才能訪問
  • Contentprovider 還能增刪本地的檔案,xml等資訊

資料存盤

存盤方式說明
SharedPreferences在鍵值對中存盤私有原始資料
內部存盤在設備記憶體中存盤私有資料
外部存盤在共享的外部存盤中存盤公共資料
SQLite 資料庫在私有資料庫中存盤結構化資料

View

ViewRoot 對應于 ViewRootImpl 類,它是連接 WindowManager 和 DecorView 的紐帶,View 的三大流程均是通過 ViewRoot 來完成的,在 ActivityThread 中,當 Activity 物件被創建完畢后,會將 DecorView 添加到 Window 中,同時會創建 ViewRootImpl 物件,并將 ViewRootImpl 物件和 DecorView 建立關聯

View 的整個繪制流程可以分為以下三個階段:

  • measure: 判斷是否需要重新計算 View 的大小,需要的話則計算
  • layout: 判斷是否需要重新計算 View 的位置,需要的話則計算
  • draw: 判斷是否需要重新繪制 View,需要的話則重繪制

MeasureSpec

MeasureSpec表示的是一個32位的整形值,它的高2位表示測量模式SpecMode,低30位表示某種測量模式下的規格大小SpecSize,MeasureSpec 是 View 類的一個靜態內部類,用來說明應該如何測量這個 View

Mode說明
UNSPECIFIED不指定測量模式, 父視圖沒有限制子視圖的大小,子視圖可以是想要的任何尺寸,通常用于系統內部,應用開發中很少用到,
EXACTLY精確測量模式,視圖寬高指定為 match_parent 或具體數值時生效,表示父視圖已經決定了子視圖的精確大小,這種模式下 View 的測量值就是 SpecSize 的值
AT_MOST最大值測量模式,當視圖的寬高指定為 wrap_content 時生效,此時子視圖的尺寸可以是不超過父視圖允許的最大尺寸的任何尺寸

對于 DecorView 而言,它的MeasureSpec 由視窗尺寸和其自身的 LayoutParams 共同決定;對于普通的 View,它的 MeasureSpec 由父視圖的 MeasureSpec 和其自身的 LayoutParams 共同決定

childLayoutParams/parentSpecModeEXACTLYAT_MOST
dp/pxEXACTLY(childSize)EXACTLY(childSize)
match_parentEXACTLY(childSize)AT_MOST(parentSize)
wrap_contentAT_MOST(parentSize)AT_MOST(parentSize)

直接繼承 View 的控制元件需要重寫 onMeasure 方法并設定 wrap_content 時的自身大小,因為 View 在布局中使用 wrap_content,那么它的 specMode 是 AT_MOST 模式,在這種模式下,它的寬/高等于父容器當前剩余的空間大小,就相當于使用 match_parent,這解決方式如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
    // 在 wrap_content 的情況下指定內部寬/高(mWidth 和 mHeight`)
    if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth, mHeight);
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
        setMeasureDimension(mWidth, heightSpecSize);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasureDimension(widthSpecSize, mHeight);
    }
}

MotionEvent

事件說明
ACTION_DOWN手指剛接觸到螢屏
ACTION_MOVE手指在螢屏上移動
ACTION_UP手機從螢屏上松開的一瞬間
ACTION_CANCEL觸摸事件取消

點擊螢屏后松開,事件序列為 DOWN -> UP,點擊螢屏滑動松開,事件序列為 DOWN -> MOVE -> ...> MOVE -> UP,

getX/getY 回傳相對于當前View左上角的坐標,getRawX/getRawY 回傳相對于螢屏左上角的坐標

TouchSlop是系統所能識別出的被認為滑動的最小距離,不同設備值可能不相同,可通過 ViewConfiguration.get(getContext()).getScaledTouchSlop() 獲取,

VelocityTracker

VelocityTracker 可用于追蹤手指在滑動中的速度:

view.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        VelocityTracker velocityTracker = VelocityTracker.obtain();
        velocityTracker.addMovement(event);
        velocityTracker.computeCurrentVelocity(1000);
        int xVelocity = (int) velocityTracker.getXVelocity();
        int yVelocity = (int) velocityTracker.getYVelocity();
        velocityTracker.clear();
        velocityTracker.recycle();
        return false;
    }
});

GestureDetector

GestureDetector 輔助檢測用戶的單擊、滑動、長按、雙擊等行為:

final GestureDetector mGestureDetector = new GestureDetector(this, new GestureDetector.OnGestureListener() {
    @Override
    public boolean onDown(MotionEvent e) { return false; }

    @Override
    public void onShowPress(MotionEvent e) { }

    @Override
    public boolean onSingleTapUp(MotionEvent e) { return false; }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return false; }

    @Override
    public void onLongPress(MotionEvent e) { }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; }
});
mGestureDetector.setOnDoubleTapListener(new OnDoubleTapListener() {
    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) { return false; }

    @Override
    public boolean onDoubleTap(MotionEvent e) { return false; }

    @Override
    public boolean onDoubleTapEvent(MotionEvent e) { return false; }
});
// 解決長按螢屏后無法拖動的問題
mGestureDetector.setIsLongpressEnabled(false);
imageView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        return mGestureDetector.onTouchEvent(event);
    }
});

如果是監聽滑動相關,建議在 onTouchEvent 中實作,如果要監聽雙擊,那么就使用 GestureDectector

Scroller

彈性滑動物件,用于實作 View 的彈性滑動,Scroller 本身無法讓 View 彈性滑動,需要和 View 的 computeScroll 方法配合使用,startScroll方法是無法讓 View 滑動的,invalidate 會導致 View 重繪,重回后會在 draw 方法中又會去呼叫 computeScroll 方法,computeScroll 方法又會去向 Scroller 獲取當前的 scrollX 和 scrollY,然后通過 scrollTo 方法實作滑動,接著又呼叫 postInvalidate 方法如此反復,

Scroller mScroller = new Scroller(mContext);

private void smoothScrollTo(int destX) {
    int scrollX = getScrollX();
    int delta = destX - scrollX;
    // 1000ms 內滑向 destX,效果就是慢慢滑動
    mScroller.startScroll(scrollX, 0 , delta, 0, 1000);
    invalidate();
}

@Override
public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
        postInvalidate();
    }
}

View 的滑動

  • scrollTo/scrollBy
    適合對 View 內容的滑動,scrollBy 實際上也是呼叫了 scrollTo 方法:
public void scrollTo(int x, int y) {
    if (mScrollX != x || mScrollY != y) {
        int oldX = mScrollX;
        int oldY = mScrollY;
        mScrollX = x;
        mScrollY = y;
        invalidateParentCaches();
        onScrollChanged(mScrollX, mScrollY, oldX, oldY);
        if (!awakenScrollBars()) {
            postInvalidateOnAnimation();
        }
    }
}

public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

mScrollX的值等于 View 的左邊緣和 View 內容左邊緣在水平方向的距離,mScrollY的值等于 View 上邊緣和 View 內容上邊緣在豎直方向的距離,scrollToscrollBy 只能改變 View 內容的位置而不能改變 View 在布局中的位置,

  • 使用影片
    操作簡單,主要適用于沒有互動的 View 和實作復雜的影片效果,
  • 改變布局引數 操作稍微復雜,適用于有互動的 View.
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
params.width += 100;
params.leftMargin += 100;
view.requestLayout();
//或者 view.setLayoutParams(params);

View 的事件分發

點擊事件達到頂級 View(一般是一個 ViewGroup),會呼叫 ViewGroup 的 dispatchTouchEvent 方法,如果頂級 ViewGroup 攔截事件即 onInterceptTouchEvent 回傳 true,則事件由 ViewGroup 處理,這時如果 ViewGroup 的 mOnTouchListener 被設定,則 onTouch 會被呼叫,否則 onTouchEvent 會被呼叫,也就是說如果都提供的話,onTouch 會屏蔽掉 onTouchEvent,在 onTouchEvent 中,如果設定了 mOnClickListenser,則 onClick 會被呼叫,如果頂級 ViewGroup 不攔截事件,則事件會傳遞給它所在的點擊事件鏈上的子 View,這時子 View 的 dispatchTouchEvent 會被呼叫,如此回圈,

  • ViewGroup 默認不攔截任何事件,ViewGroup 的 onInterceptTouchEvent 方法默認回傳 false,
  • View 沒有 onInterceptTouchEvent 方法,一旦有點擊事件傳遞給它,onTouchEvent 方法就會被呼叫,
  • View 在可點擊狀態下,onTouchEvent 默認會消耗事件,
  • ACTION_DOWN 被攔截了,onInterceptTouchEvent 方法執行一次后,就會留下記號(mFirstTouchTarget == null)那么往后的 ACTION_MOVE 和 ACTION_UP 都會攔截,`

在 Activity 中獲取某個 View 的寬高

  • Activity/View#onWindowFocusChanged
// 此時View已經初始化完畢
// 當Activity的視窗得到焦點和失去焦點時均會被呼叫一次
// 如果頻繁地進行onResume和onPause,那么onWindowFocusChanged也會被頻繁地呼叫
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if (hasFocus) {
        int width = view.getMeasureWidth();
        int height = view.getMeasuredHeight();
    }
}
  • view.post(runnable)
// 通過post可以將一個runnable投遞到訊息佇列的尾部,// 然后等待Looper呼叫次runnable的時候,View也已經初
// 始化好了
protected void onStart() {
    super.onStart();
    view.post(new Runnable() {

        @Override
        public void run() {
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    });
}
  • ViewTreeObserver
// 當View樹的狀態發生改變或者View樹內部的View的可見// 性發生改變時,onGlobalLayout方法將被回呼
protected void onStart() {
    super.onStart();

    ViewTreeObserver observer = view.getViewTreeObserver();
    observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {

        @SuppressWarnings("deprecation")
        @Override
        public void onGlobalLayout() {
            view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    });
}

Draw 的基本流程

// 繪制基本上可以分為六個步驟
public void draw(Canvas canvas) {
    ...
    // 步驟一:繪制View的背景
    drawBackground(canvas);
    ...
    // 步驟二:如果需要的話,保持canvas的圖層,為fading做準備
    saveCount = canvas.getSaveCount();
    ...
    canvas.saveLayer(left, top, right, top + length, null, flags);
    ...
    // 步驟三:繪制View的內容
    onDraw(canvas);
    ...
    // 步驟四:繪制View的子View
    dispatchDraw(canvas);
    ...
    // 步驟五:如果需要的話,繪制View的fading邊緣并恢復圖層
    canvas.drawRect(left, top, right, top + length, p);
    ...
    canvas.restoreToCount(saveCount);
    ...
    // 步驟六:繪制View的裝飾(例如滾動條等等)
    onDrawForeground(canvas)
}

自定義 View

  • 繼承 View 重寫 onDraw 方法

主要用于實作一些不規則的效果,靜態或者動態地顯示一些不規則的圖形,即重寫 onDraw 方法,采用這種方式需要自己支持 wrap_content,并且 padding 也需要自己處理,

  • 繼承 ViewGroup 派生特殊的 Layout

主要用于實作自定義布局,采用這種方式需要合適地處理 ViewGroup 的測量、布局兩個程序,并同時處理子元素的測量和布局程序,

  • 繼承特定的 View

用于擴張某種已有的View的功能

  • 繼承特定的 ViewGroup

用于擴張某種已有的ViewGroup的功能

行程

行程(Process) 是計算機中的程式關于某資料集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是作業系統結構的基礎,

當某個應用組件啟動且該應用沒有運行其他任何組件時,Android 系統會使用單個執行執行緒為應用啟動新的 Linux 行程,默認情況下,同一應用的所有組件在相同的行程和執行緒(稱為“主”執行緒)中運行,

各類組件元素的清單檔案條目<activity><service><receiver><provider>—均支持 android:process 屬性,此屬性可以指定該組件應在哪個行程運行,

行程生命周期

1、前臺行程

  • 托管用戶正在互動的 Activity(已呼叫 Activity 的 onResume() 方法)
  • 托管某個 Service,后者系結到用戶正在互動的 Activity
  • 托管正在“前臺”運行的 Service(服務已呼叫 startForeground()
  • 托管正執行一個生命周期回呼的 Service(onCreate()onStart()onDestroy()
  • 托管正執行其 onReceive() 方法的 BroadcastReceiver

2、可見行程

  • 托管不在前臺、但仍對用戶可見的 Activity(已呼叫其 onPause() 方法),例如,如果 re前臺 Activity 啟動了一個對話框,允許在其后顯示上一 Activity,則有可能會發生這種情況,
  • 托管系結到可見(或前臺)Activity 的 Service

3、服務行程

  • 正在運行已使用 startService() 方法啟動的服務且不屬于上述兩個更高類別行程的行程,

4、后臺行程

  • 包含目前對用戶不可見的 Activity 的行程(已呼叫 Activity 的 onStop() 方法),通常會有很多后臺行程在運行,因此它們會保存在 LRU (最近最少使用)串列中,以確保包含用戶最近查看的 Activity 的行程最后一個被終止,

5、空行程

  • 不含任何活動應用組件的行程,保留這種行程的的唯一目的是用作快取,以縮短下次在其中運行組件所需的啟動時間, 為使總體系統資源在行程快取和底層內核快取之間保持平衡,系統往往會終止這些行程,\

多行程

如果注冊的四大組件中的任意一個組件時用到了多行程,運行該組件時,都會創建一個新的 Application 物件,對于多行程重復創建 Application 這種情況,只需要在該類中對當前行程加以判斷即可,

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        Log.d("MyApplication", getProcessName(android.os.Process.myPid()));
        super.onCreate();
    }

    /**
     * 根據行程 ID 獲取行程名
     * @param pid 行程id
     * @return 行程名
     */
    public  String getProcessName(int pid){
        ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningAppProcessInfo> processInfoList = am.getRunningAppProcesses();
        if (processInfoList == null) {
            return null;
        }
        for (ActivityManager.RunningAppProcessInfo processInfo : processInfoList) {
            if (processInfo.pid == pid) {
                return processInfo.processName;
            }
        }
        return null;
    }
}

一般來說,使用多行程會造成以下幾個方面的問題:

  • 靜態成員和單例模式完全失效
  • 執行緒同步機制完全失效
  • SharedPreferences 的可靠性下降
  • Application 會多次創建

行程存活

OOM_ADJ

ADJ級別取值解釋
UNKNOWN_ADJ16一般指將要會快取行程,無法獲取確定值
CACHED_APP_MAX_ADJ15不可見行程的adj最大值
CACHED_APP_MIN_ADJ9不可見行程的adj最小值
SERVICE_B_AD8B List 中的 Service(較老的、使用可能性更小)
PREVIOUS_APP_ADJ7上一個App的行程(往往通過按回傳鍵)
HOME_APP_ADJ6Home行程
SERVICE_ADJ5服務行程(Service process)
HEAVY_WEIGHT_APP_ADJ4后臺的重量級行程,system/rootdir/init.rc 檔案中設定
BACKUP_APP_ADJ3備份行程
PERCEPTIBLE_APP_ADJ2可感知行程,比如后臺音樂播放
VISIBLE_APP_ADJ1可見行程(Visible process)
FOREGROUND_APP_ADJ0前臺行程(Foreground process)
PERSISTENT_SERVICE_ADJ-11關聯著系統或persistent行程
PERSISTENT_PROC_ADJ-12系統 persistent 行程,比如telephony
SYSTEM_ADJ-16系統行程
NATIVE_ADJ-17native行程(不被系統管理)

行程被殺情況

行程保活方案

  • 開啟一個像素的 Activity
  • 使用前臺服務
  • 多行程相互喚醒
  • JobSheduler 喚醒
  • 粘性服務 & 與系統服務捆綁

Parcelable 介面

只要實作了 Parcelable 介面,一個類的物件就可以實作序列化并可以通過 Intent 和 Binder 傳遞,

使用示例

import android.os.Parcel;
import android.os.Parcelable;

public class User implements Parcelable {
    
    private int userId;

    protected User(Parcel in) {
        userId = in.readInt();
    }

    public static final Creator<User> CREATOR = new Creator<User>() {
        @Override
        public User createFromParcel(Parcel in) {
            return new User(in);
        }

        @Override
        public User[] newArray(int size) {
            return new User[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(userId);
    }

    public int getUserId() {
        return userId;
    }
}

方法說明

Parcel 內部包裝了可序列化的資料,可以在 Binder 中自由傳輸,序列化功能由 writeToParcel 方法完成,最終是通過 Parcel 中的一系列 write 方法完成,反序列化功能由 CREATOR 來完成,通過 Parcel 的一系列 read 方法來完成反序列化程序,

方法功能
createFromParcel(Parcel in)從序列化后的物件中創建原始物件
newArray(int size)創建指定長度的原始物件陣列
User(Parcel in)從序列化后的物件中創建原始物件
writeToParcel(Parcel dest, int flags)將當前物件寫入序列化結構中,其中 flags 標識有兩種值:0 或者 1,為 1 時標識當前物件需要作為回傳值回傳,不能立即釋放資源,幾乎所有情況都為 0
describeContents回傳當前物件的內容描述,如果含有檔案描述符,回傳 1,否則回傳 0,幾乎所有情況都回傳 0

Parcelable 與 Serializable 對比

  • Serializable 使用 I/O 讀寫存盤在硬碟上,而 Parcelable 是直接在記憶體中讀寫
  • Serializable 會使用反射,序列化和反序列化程序需要大量 I/O 操作, Parcelable 自已實作封送和解封(marshalled &unmarshalled)操作不需要用反射,資料也存放在 Native 記憶體中,效率要快很多

IPC

IPC 即 Inter-Process Communication (行程間通信),Android 基于 Linux,而 Linux 出于安全考慮,不同行程間不能之間操作對方的資料,這叫做“行程隔離”,

在 Linux 系統中,虛擬記憶體機制為每個行程分配了線性連續的記憶體空間,作業系統將這種虛擬記憶體空間映射到物理記憶體空間,每個行程有自己的虛擬記憶體空間,進而不能操作其他行程的記憶體空間,只有作業系統才有權限操作物理記憶體空間, 行程隔離保證了每個行程的記憶體安全,

IPC方式

名稱優點缺點適用場景
Bundle簡單易用只能傳輸 Bundle 支持的資料型別四大組件間的行程間通信
檔案共享簡單易用不適合高并發場景,并且無法做到行程間即時通信無并發訪問情形,交換簡單的資料實時性不高的場景
AIDL功能強大,支持一對多并發通信,支持實時通信使用稍復雜,需要處理好執行緒同步一對多通信且有 RPC 需求
Messenger功能一般,支持一對多串行通信,支持實時通信不能很處理高并發清醒,不支持 RPC,資料通過 Message 進行傳輸,因此只能傳輸 Bundle 支持的資料型別低并發的一對多即時通信,無RPC需求,或者無需回傳結果的RPC需求
ContentProvider在資料源訪問方面功能強大,支持一對多并發資料共享,可通過 Call 方法擴展其他操作可以理解為受約束的 AIDL,主要提供資料源的 CRUD 操作一對多的行程間資料共享
Socket可以通過網路傳輸位元組流,支持一對多并發實時通信實作細節稍微有點煩瑣,不支持直接的RPC網路資料交換

Binder

Binder 是 Android 中的一個類,實作了 IBinder 介面,從 IPC 角度來說,Binder 是 Android 中的一種擴行程通信方方式,從 Android 應用層來說,Binder 是客戶端和服務器端進行通信的媒介,當 bindService 的時候,服務端會回傳一個包含了服務端業務呼叫的 Binder 物件,

Binder 相較于傳統 IPC 來說更適合于Android系統,具體原因的包括如下三點:

  • Binder 本身是 C/S 架構的,這一點更符合 Android 系統的架構
  • 性能上更有優勢:管道,訊息佇列,Socket 的通訊都需要兩次資料拷貝,而 Binder 只需要一次,要知道,對于系統底層的 IPC 形式,少一次資料拷貝,對整體性能的影響是非常之大的
  • 安全性更好:傳統 IPC 形式,無法得到對方的身份標識(UID/GID),而在使用 Binder IPC 時,這些身份標示是跟隨呼叫程序而自動傳遞的,Server 端很容易就可以知道 Client 端的身份,非常便于做安全檢查

示例:

  • 新建AIDL介面檔案

RemoteService.aidl

package com.example.mystudyapplication3;

interface IRemoteService {

    int getUserId();

}

系統會自動生成 IRemoteService.java:

/*
 * This file is auto-generated.  DO NOT MODIFY.
 */
package com.example.mystudyapplication3;
// Declare any non-default types here with import statements
//import com.example.mystudyapplication3.IUserBean;

public interface IRemoteService extends android.os.IInterface {
    /**
     * Local-side IPC implementation stub class.
     */
    public static abstract class Stub extends android.os.Binder implements com.example.mystudyapplication3.IRemoteService {
        private static final java.lang.String DESCRIPTOR = "com.example.mystudyapplication3.IRemoteService";

        /**
         * Construct the stub at attach it to the interface.
         */
        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }

        /**
         * Cast an IBinder object into an com.example.mystudyapplication3.IRemoteService interface,
         * generating a proxy if needed.
         */
        public static com.example.mystudyapplication3.IRemoteService asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.example.mystudyapplication3.IRemoteService))) {
                return ((com.example.mystudyapplication3.IRemoteService) iin);
            }
            return new com.example.mystudyapplication3.IRemoteService.Stub.Proxy(obj);
        }

        @Override
        public android.os.IBinder asBinder() {
            return this;
        }

        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            java.lang.String descriptor = DESCRIPTOR;
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(descriptor);
                    return true;
                }
                case TRANSACTION_getUserId: {
                    data.enforceInterface(descriptor);
                    int _result = this.getUserId();
                    reply.writeNoException();
                    reply.writeInt(_result);
                    return true;
                }
                default: {
                    return super.onTransact(code, data, reply, flags);
                }
            }
        }

        private static class Proxy implements com.example.mystudyapplication3.IRemoteService {
            private android.os.IBinder mRemote;

            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }

            @Override
            public android.os.IBinder asBinder() {
                return mRemote;
            }

            public java.lang.String getInterfaceDescriptor() {
                return DESCRIPTOR;
            }

            @Override
            public int getUserId() throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                int _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    mRemote.transact(Stub.TRANSACTION_getUserId, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readInt();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }
        }

        static final int TRANSACTION_getUserId = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
    }

    public int getUserId() throws android.os.RemoteException;
}
方法含義
DESCRIPTORBinder 的唯一標識,一般用當前的 Binder 的類名表示
asInterface(IBinder obj)將服務端的 Binder 物件成客戶端所需的 AIDL 介面型別物件,這種轉換程序是區分行程的,如果位于同一行程,回傳的就是 Stub 物件本身,否則回傳的是系統封裝后的 Stub.proxy 物件,
asBinder用于回傳當前 Binder 物件
onTransact運行在服務端中的 Binder 執行緒池中,遠程請求會通過系統底層封裝后交由此方法來處理
定向 tag含義
in資料只能由客戶端流向服務端,服務端將會收到客戶端物件的完整資料,客戶端物件不會因為服務端對傳參的修改而發生變動,
out資料只能由服務端流向客戶端,服務端將會收到客戶端物件,該物件不為空,但是它里面的欄位為空,但是在服務端對該物件作任何修改之后客戶端的傳參物件都會同步改動,
inout服務端將會接收到客戶端傳來物件的完整資訊,并且客戶端將會同步服務端對該物件的任何變動,

流程

AIDL 通信

Android Interface Definition Language

使用示例:

  • 新建AIDL介面檔案
// RemoteService.aidl
package com.example.mystudyapplication3;

interface IRemoteService {

    int getUserId();

}
  • 創建遠程服務
public class RemoteService extends Service {

    private int mId = -1;

    private Binder binder = new IRemoteService.Stub() {

        @Override
        public int getUserId() throws RemoteException {
            return mId;
        }
    };

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        mId = 1256;
        return binder;
    }
}
  • 宣告遠程服務
<service
    android:name=".RemoteService"
    android:process=":aidl" />
  • 系結遠程服務
public class MainActivity extends AppCompatActivity {

    public static final String TAG = "wzq";

    IRemoteService iRemoteService;
    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            iRemoteService = IRemoteService.Stub.asInterface(service);
            try {
                Log.d(TAG, String.valueOf(iRemoteService.getUserId()));
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            iRemoteService = null;
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        bindService(new Intent(MainActivity.this, RemoteService.class), mConnection, Context.BIND_AUTO_CREATE);
    }
}

Messenger

Messenger可以在不同行程中傳遞 Message 物件,在Message中放入我們需要傳遞的資料,就可以輕松地實作資料的行程間傳遞了,Messenger 是一種輕量級的 IPC 方案,底層實作是 AIDL,

Window / WindowManager

Window 概念與分類

Window 是一個抽象類,它的具體實作是 PhoneWindow,WindowManager 是外界訪問 Window 的入口,Window 的具體實作位于 WindowManagerService 中,WindowManager 和 WindowManagerService 的互動是一個 IPC 程序,Android 中所有的視圖都是通過 Window 來呈現,因此 Window 實際是 View 的直接管理者,

Window 型別說明層級
Application Window對應著一個 Activity1~99
Sub Window不能單獨存在,只能附屬在父 Window 中,如 Dialog 等1000~1999
System Window需要權限宣告,如 Toast 和 系統狀態欄等2000~2999

Window 的內部機制

Window 是一個抽象的概念,每一個 Window 對應著一個 View 和一個 ViewRootImpl,Window 實際是不存在的,它是以 View 的形式存在,對 Window 的訪問必須通過 WindowManager,WindowManager 的實作類是 WindowManagerImpl:

WindowManagerImpl.java

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.updateViewLayout(view, params);
}

@Override
public void removeView(View view) {
    mGlobal.removeView(view, false);
}

WindowManagerImpl 沒有直接實作 Window 的三大操作,而是全部交給 WindowManagerGlobal 處理,WindowManagerGlobal 以工廠的形式向外提供自己的實體:

WindowManagerGlobal.java

// 添加
public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    ···
    // 子 Window 的話需要調整一些布局引數
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    if (parentWindow != null) {
        parentWindow.adjustLayoutParamsForSubWindow(wparams);
    } else {
        ···
    }
    ViewRootImpl root;
    View panelParentView = null;
    synchronized (mLock) {
        // 新建一個 ViewRootImpl,并通過其 setView 來更新界面完成 Window 的添加程序
        ···
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            if (index >= 0) {
                removeViewLocked(index, true);
            }
            throw e;
        }
    }
}

// 洗掉
@UnsupportedAppUsage
public void removeView(View view, boolean immediate) {
    ···
    synchronized (mLock) {
        int index = findViewLocked(view, true);
        View curView = mRoots.get(index).getView();
        removeViewLocked(index, immediate);
        ···
    }
}

private void removeViewLocked(int index, boolean immediate) {
    ViewRootImpl root = mRoots.get(index);
    View view = root.getView();
    if (view != null) {
        InputMethodManager imm = InputMethodManager.getInstance();
        if (imm != null) {
            imm.windowDismissed(mViews.get(index).getWindowToken());
        }
    }
    boolean deferred = root.die(immediate);
    if (view != null) {
        view.assignParent(null);
        if (deferred) {
            mDyingViews.add(view);
        }
    }
}

// 更新
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
    ···
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
    view.setLayoutParams(wparams);
    synchronized (mLock) {
        int index = findViewLocked(view, true);
        ViewRootImpl root = mRoots.get(index);
        mParams.remove(index);
        mParams.add(index, wparams);
        root.setLayoutParams(wparams, false);
    }
}

在 ViewRootImpl 中最侄訓通過 WindowSession 來完成 Window 的添加、更新、洗掉作業,mWindowSession 的型別是 IWindowSession,是一個 Binder 物件,真正地實作類是 Session,是一個 IPC 程序,

Window 的創建程序

Activity 的 Window 創建程序

在 Activity 的創建程序中,最侄訓由 ActivityThread 的 performLaunchActivity() 來完成整個啟動程序,該方法內部會通過類加載器創建 Activity 的實體物件,并呼叫 attach 方法關聯一系列背景關系環境變數,在 Activity 的 attach 方法里,系統會創建所屬的 Window 物件并設定回呼介面,然后在 Activity 的 setContentView 方法中將視圖附屬在 Window 上:

Activity.java

final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor,
        Window window, ActivityConfigCallback activityConfigCallback) {
    attachBaseContext(context);

    mFragments.attachHost(null /*parent*/);

    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    mWindow.setWindowControllerCallback(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    mWindow.getLayoutInflater().setPrivateFactory(this);
    if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
        mWindow.setSoftInputMode(info.softInputMode);
    }
    if (info.uiOptions != 0) {
        mWindow.setUiOptions(info.uiOptions);
    }
    ···
}
···

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

PhoneWindow.java

@Override
public void setContentView(int layoutResID) {
    if (mContentParent == null) { // 如果沒有 DecorView,就創建
        installDecor();
    } else {
        mContentParent.removeAllViews();
    }
    mLayoutInflater.inflate(layoutResID, mContentParent);
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        // 回呼 Activity 的 onContentChanged 方法通知 Activity 視圖已經發生改變
        cb.onContentChanged();
    }
}

這個時候 DecorView 還沒有被 WindowManager 正式添加,在 ActivityThread 的 handleResumeActivity 方法中,首先會呼叫 Activity 的 onResume 方法,接著呼叫 Activity 的 makeVisible(),完成 DecorView 的添加和顯示程序:

Activity.java

void makeVisible() {
    if (!mWindowAdded) {
        ViewManager wm = getWindowManager();
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}

Dialog 的 Window 創建程序

Dialog 的 Window 的創建程序和 Activity 類似,創建同樣是通過 PolicyManager 的 makeNewWindow 方法完成的,創建后的物件實際就是 PhoneWindow,當 Dialog 被關閉時,會通過 WindowManager 來移除 DecorView:mWindowManager.removeViewImmediate(mDecor),

Dialog.java

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean      createContextThemeWrapper) {
    ···
    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

    final Window w = new PhoneWindow(mContext);
    mWindow = w;
    w.setCallback(this);
    w.setOnWindowDismissedCallback(this);
    w.setOnWindowSwipeDismissedCallback(() -> {
        if (mCancelable) {
            cancel();
        }
    });
    w.setWindowManager(mWindowManager, null, null);
    w.setGravity(Gravity.CENTER);

    mListenersHandler = new ListenersHandler(this);
}

普通 Dialog 必須采用 Activity 的 Context,采用 Application 的 Context 就會報錯,是因為應用 token 所導致,應用 token 一般只有 Activity 擁有,系統 Window 比較特殊,不需要 token,

Toast 的 Window 創建程序

Toast 屬于系統 Window ,由于其具有定時取消功能,所以系統采用了 Handler,Toast 的內部有兩類 IPC 程序,第一類是 Toast 訪問 NotificationManagerService,第二類是 NotificationManagerService 回呼 Toast 里的 TN 介面,

Toast 內部的視圖由兩種方式,一種是系統默認的樣式,另一種是 setView 指定一個自定義 View,它們都對應 Toast 的一個內部成員 mNextView,

Toast.java

public void show() {
    if (mNextView == null) {
        throw new RuntimeException("setView must have been called");
    }

    INotificationManager service = getService();
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;

    try {
        service.enqueueToast(pkg, tn, mDuration);
    } catch (RemoteException e) {
        // Empty
    }
}
···

public void cancel() {
    mTN.cancel();
}

NotificationManagerService.java

private void showNextToastLocked() {
    ToastRecord record = mToastQueue.get(0);
    while (record != null) {
        if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
        try {
            record.callback.show();
            scheduleTimeoutLocked(record, false);
            return;
        } catch (RemoteException e) {
            Slog.w(TAG, "Object died trying to show notification " + record.callback
                    + " in package " + record.pkg);
            // remove it from the list and let the process die
            int index = mToastQueue.indexOf(record);
            if (index >= 0) {
                mToastQueue.remove(index);
            }
            keepProcessAliveLocked(record.pid);
            if (mToastQueue.size() > 0) {
                record = mToastQueue.get(0);
            } else {
                record = null;
            }
        }
    }
}

···
private void scheduleTimeoutLocked(ToastRecord r, boolean immediate)
{
    Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
    long delay = immediate ? 0 : (r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY);
    mHandler.removeCallbacksAndMessages(r);
    mHandler.sendMessageDelayed(m, delay);
}

Bitmap

配置資訊與壓縮方式

Bitmap 中有兩個內部列舉類:

  • Config 是用來設定顏色配置資訊
  • CompressFormat 是用來設定壓縮方式
Config單位像素所占位元組數決議
Bitmap.Config.ALPHA_81顏色資訊只由透明度組成,占8位
Bitmap.Config.ARGB_44442顏色資訊由rgba四部分組成,每個部分都占4位,總共占16位
Bitmap.Config.ARGB_88884顏色資訊由rgba四部分組成,每個部分都占8位,總共占32位,是Bitmap默認的顏色配置資訊,也是最占空間的一種配置
Bitmap.Config.RGB_5652顏色資訊由rgb三部分組成,R占5位,G占6位,B占5位,總共占16位
RGBA_F168Android 8.0 新增(更豐富的色彩表現HDR)
HARDWARESpecialAndroid 8.0 新增 (Bitmap直接存盤在graphic memory)

通常我們優化 Bitmap 時,當需要做性能優化或者防止 OOM,我們通常會使用 Bitmap.Config.RGB_565 這個配置,因為 Bitmap.Config.ALPHA_8 只有透明度,顯示一般圖片沒有意義,Bitmap.Config.ARGB_4444 顯示圖片不清楚, Bitmap.Config.ARGB_8888 占用記憶體最多,

CompressFormat決議
Bitmap.CompressFormat.JPEG表示以 JPEG 壓縮演算法進行影像壓縮,壓縮后的格式可以是 .jpg 或者 .jpeg,是一種有損壓縮
Bitmap.CompressFormat.PNG顏色資訊由 rgba 四部分組成,每個部分都占 4 位,總共占 16 位
Bitmap.Config.ARGB_8888顏色資訊由 rgba 四部分組成,每個部分都占 8 位,總共占 32 位,是 Bitmap 默認的顏色配置資訊,也是最占空間的一種配置
Bitmap.Config.RGB_565顏色資訊由 rgb 三部分組成,R 占 5 位,G 占 6 位,B 占 5 位,總共占 16 位

常用操作

裁剪、縮放、旋轉、移動

Matrix matrix = new Matrix();  
// 縮放 
matrix.postScale(0.8f, 0.9f);  
// 左旋,引數為正則向右旋
matrix.postRotate(-45);  
// 平移, 在上一次修改的基礎上進行再次修改 set 每次操作都是最新的 會覆寫上次的操作
matrix.postTranslate(100, 80);
// 裁剪并執行以上操作
Bitmap bitmap = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, true);

雖然Matrix還可以呼叫postSkew方法進行傾斜操作,但是卻不可以在此時創建Bitmap時使用,

Bitmap與Drawable轉換

// Drawable -> Bitmap
public static Bitmap drawableToBitmap(Drawable drawable) {
    Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565);
    Canvas canvas = new Canvas(bitmap);
    drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight();
    drawable.draw(canvas);
    return bitmap;
}

// Bitmap -> Drawable
public static Drawable bitmapToDrawable(Resources resources, Bitmap bm) {
    Drawable drawable = new BitmapDrawable(resources, bm);
    return drawable;
}

保存與釋放

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
File file = new File(getFilesDir(),"test.jpg");
if(file.exists()){
    file.delete();
}
try {
    FileOutputStream outputStream=new FileOutputStream(file);
    bitmap.compress(Bitmap.CompressFormat.JPEG,90,outputStream);
    outputStream.flush();
    outputStream.close();
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}
//釋放bitmap的資源,這是一個不可逆轉的操作
bitmap.recycle();

圖片壓縮

public static Bitmap compressImage(Bitmap image) {
    if (image == null) {
        return null;
    }
    ByteArrayOutputStream baos = null;
    try {
        baos = new ByteArrayOutputStream();
        image.compress(Bitmap.CompressFormat.JPEG, 100, baos);
        byte[] bytes = baos.toByteArray();
        ByteArrayInputStream isBm = new ByteArrayInputStream(bytes);
        Bitmap bitmap = BitmapFactory.decodeStream(isBm);
        return bitmap;
    } catch (OutOfMemoryError e) {
        e.printStackTrace();
    } finally {
        try {
            if (baos != null) {
                baos.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return null;
}

BitmapFactory

Bitmap創建流程

Option類

常用方法說明
boolean inJustDecodeBounds如果設定為true,不獲取圖片,不分配記憶體,但會回傳圖片的高度寬度資訊
int inSampleSize圖片縮放的倍數
int outWidth獲取圖片的寬度值
int outHeight獲取圖片的高度值
int inDensity用于位圖的像素壓縮比
int inTargetDensity用于目標位圖的像素壓縮比(要生成的位圖)
byte[] inTempStorage創建臨時檔案,將圖片存盤
boolean inScaled設定為true時進行圖片壓縮,從inDensity到inTargetDensity
boolean inDither如果為true,解碼器嘗試抖動解碼
Bitmap.Config inPreferredConfig設定解碼器這個值是設定色彩模式,默認值是ARGB_8888,在這個模式下,一個像素點占用4bytes空間,一般對透明度不做要求的話,一般采用RGB_565模式,這個模式下一個像素點占用2bytes
String outMimeType設定解碼影像
boolean inPurgeable當存盤Pixel的記憶體空間在系統記憶體不足時是否可以被回收
boolean inInputShareableinPurgeable為true情況下才生效,是否可以共享一個InputStream
boolean inPreferQualityOverSpeed為true則優先保證Bitmap質量其次是解碼速度
boolean inMutable配置Bitmap是否可以更改,比如:在Bitmap上隔幾個像素加一條線段
int inScreenDensity當前螢屏的像素密度

基本使用

try {
    FileInputStream fis = new FileInputStream(filePath);
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    // 設定inJustDecodeBounds為true后,再使用decodeFile()等方法,并不會真正的分配空間,即解碼出來的Bitmap為null,但是可計算出原始圖片的寬度和高度,即options.outWidth和options.outHeight
    BitmapFactory.decodeFileDescriptor(fis.getFD(), null, options);
    float srcWidth = options.outWidth;
    float srcHeight = options.outHeight;
    int inSampleSize = 1;

    if (srcHeight > height || srcWidth > width) {
        if (srcWidth > srcHeight) {
            inSampleSize = Math.round(srcHeight / height);
        } else {
            inSampleSize = Math.round(srcWidth / width);
        }
    }

    options.inJustDecodeBounds = false;
    options.inSampleSize = inSampleSize;

    return BitmapFactory.decodeFileDescriptor(fis.getFD(), null, options);
} catch (Exception e) {
    e.printStackTrace();
}

記憶體回收

if(bitmap != null && !bitmap.isRecycled()){ 
    // 回收并且置為null
    bitmap.recycle(); 
    bitmap = null; 
} 

Bitmap 類的構造方法都是私有的,所以開發者不能直接 new 出一個 Bitmap 物件,只能通過 BitmapFactory 類的各種靜態方法來實體化一個 Bitmap,仔細查看 BitmapFactory 的源代碼可以看到,生成 Bitmap 物件最終都是通過 JNI 呼叫方式實作的,所以,加載 Bitmap 到記憶體里以后,是包含兩部分記憶體區域的,簡單的說,一部分是Java 部分的,一部分是 C 部分的,這個 Bitmap 物件是由 Java 部分分配的,不用的時候系統就會自動回收了,但是那個對應的 C 可用的記憶體區域,虛擬機是不能直接回收的,這個只能呼叫底層的功能釋放,所以需要呼叫 recycle() 方法來釋放 C 部分的記憶體,從 Bitmap 類的源代碼也可以看到,recycle() 方法里也的確是呼叫了 JNI 方法了的,

螢屏適配

單位

  • dpi 每英寸像素數(dot per inch)
  • dp
    密度無關像素 - 一種基于螢屏物理密度的抽象單元, 這些單位相對于 160 dpi 的螢屏,因此一個 dp 是 160 dpi 螢屏上的一個 px, dp 與像素的比率將隨著螢屏密度而變化,但不一定成正比,為不同設備的 UI 元素的實際大小提供了一致性,
  • sp
    與比例無關的像素 - 這與 dp 單位類似,但它也可以通過用戶的字體大小首選項進行縮放,建議在指定字體大小時使用此單位,以便根據螢屏密度和用戶偏好調整它們,
dpi = px / inch

density = dpi / 160

dp = px / density

頭條適配方案

private static void setCustomDensity(@NonNull Activity activity, @NonNull final Application application) {
    final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();
    if (sNoncompatDensity == 0) {
        sNoncompatDensity = appDisplayMetrics.density;
        sNoncompatScaledDensity = appDisplayMetrics.scaledDensity;
        // 監聽字體切換
        application.registerComponentCallbacks(new ComponentCallbacks() {
            @Override
            public void onConfigurationChanged(Configuration newConfig) {
                if (newConfig != null && newConfig.fontScale > 0) {
                    sNoncompatScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
                }
            }

            @Override
            public void onLowMemory() {

            }
        });
    }
    
    // 適配后的dpi將統一為360dpi
    final float targetDensity = appDisplayMetrics.widthPixels / 360;
    final float targetScaledDensity = targetDensity * (sNoncompatScaledDensity / sNoncompatDensity);
    final int targetDensityDpi = (int)(160 * targetDensity);

    appDisplayMetrics.density = targetDensity;
    appDisplayMetrics.scaledDensity = targetScaledDensity;
    appDisplayMetrics.densityDpi = targetDensityDpi;

    final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
    activityDisplayMetrics.density = targetDensity;
    activityDisplayMetrics.scaledDensity = targetScaledDensity;
    activityDisplayMetrics.densityDpi = targetDensityDpi
}

劉海屏適配

  • Android P 劉海屏適配方案

Android P 支持最新的全面屏以及為攝像頭和揚聲器預留空間的凹口螢屏,通過全新的 DisplayCutout 類,可以確定非功能區域的位置和形狀,這些區域不應顯示內容,要確定這些凹口螢屏區域是否存在及其位置,使用 getDisplayCutout() 函式,

DisplayCutout 類方法說明
getBoundingRects()回傳Rects的串列,每個Rects都是顯示屏上非功能區域的邊界矩形
getSafeInsetLeft ()回傳安全區域距離螢屏左邊的距離,單位是px
getSafeInsetRight ()回傳安全區域距離螢屏右邊的距離,單位是px
getSafeInsetTop ()回傳安全區域距離螢屏頂部的距離,單位是px
getSafeInsetBottom()回傳安全區域距離螢屏底部的距離,單位是px

Android P 中 WindowManager.LayoutParams 新增了一個布局引數屬性 layoutInDisplayCutoutMode:

模式模式說明
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT只有當DisplayCutout完全包含在系統欄中時,才允許視窗延伸到DisplayCutout區域, 否則,視窗布局不與DisplayCutout區域重疊,
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER該視窗決不允許與DisplayCutout區域重疊,
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES該視窗始終允許延伸到螢屏短邊上的DisplayCutout區域,
  • Android P 之前的劉海屏適配

不同廠商的劉海屏適配方案不盡相同,需分別查閱各自的開發者檔案,

Context

Context 本身是一個抽象類,是對一系列系統服務介面的封裝,包括:內部資源、包、類加載、I/O操作、權限、主執行緒、IPC 和組件啟動等操作的管理,ContextImpl, Activity, Service, Application 這些都是 Context 的直接或間接子類, 關系如下:

ContextWrapper是代理Context的實作,簡單地將其所有呼叫委托給另一個Context(mBase),

Application、Activity、Service通過attach() 呼叫父類ContextWrapper的attachBaseContext(), 從而設定父類成員變數 mBase 為 ContextImpl 物件, ContextWrapper 的核心作業都是交給 mBase(ContextImpl) 來完成,這樣可以子類化 Context 以修改行為而無需更改原始 Context,

SharedPreferences

SharedPreferences 采用key-value(鍵值對)形式, 主要用于輕量級的資料存盤, 尤其適合保存應用的配置引數, 但不建議使用 SharedPreferences 來存盤大規模的資料, 可能會降低性能.

SharedPreferences采用xml檔案格式來保存資料, 該檔案所在目錄位于 /data/data/<package name>/shared_prefs,如:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
   <string name="blog">https://github.com/JasonWu1111/Android-Review</string>
</map>

從Android N開始, 創建的 SP 檔案模式, 不允許 MODE_WORLD_READABLEMODE_WORLD_WRITEABLE 模塊, 否則會直接拋出例外 SecurityException, MODE_MULTI_PROCESS 這種多行程的方式也是 Google 不推薦的方式, 后續同樣會不再支持,

當設定 MODE_MULTI_PROCESS 模式, 則每次 getSharedPreferences 程序, 會檢查 SP 檔案上次修改時間和檔案大小, 一旦所有修改則會重新從磁盤加載檔案,

獲取方式

getPreferences

Activity.getPreferences(mode): 以當前 Activity 的類名作為 SP 的檔案名. 即 xxxActivity.xml Activity.java

public SharedPreferences getPreferences(int mode) {
    return getSharedPreferences(getLocalClassName(), mode);
}

getDefaultSharedPreferences

PreferenceManager.getDefaultSharedPreferences(Context): 以包名加上 _preferences 作為檔案名, 以 MODE_PRIVATE 模式創建 SP 檔案. 即 packgeName_preferences.xml.

public static SharedPreferences getDefaultSharedPreferences(Context context) {
    return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
           getDefaultSharedPreferencesMode());
}

getSharedPreferences

直接呼叫 Context.getSharedPreferences(name, mode),所有的方法最終都是呼叫到如下方法:

class ContextImpl extends Context {
    private ArrayMap<String, File> mSharedPrefsPaths;

    public SharedPreferences getSharedPreferences(String name, int mode) {
        File file;
        synchronized (ContextImpl.class) {
            if (mSharedPrefsPaths == null) {
                mSharedPrefsPaths = new ArrayMap<>();
            }
            //先從mSharedPrefsPaths查詢是否存在相應檔案
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
                //如果檔案不存在, 則創建新的檔案 
                file = getSharedPreferencesPath(name);
                mSharedPrefsPaths.put(name, file);
            }
        }
 
        return getSharedPreferences(file, mode);
    }
}

架構

SharedPreferences 與 Editor 只是兩個介面. SharedPreferencesImpl 和 EditorImpl 分別實作了對應介面,另外, ContextImpl 記錄著 SharedPreferences 的重要資料,

putxxx() 操作把資料寫入到EditorImpl.mModified;

apply()/commit() 操作先呼叫 commitToMemory(), 將資料同步到 SharedPreferencesImpl 的 mMap, 并保存到 MemoryCommitResult 的 mapToWriteToDisk,再呼叫 enqueueDiskWrite(), 寫入到磁盤檔案; 先之前把原有資料保存到 .bak 為后綴的檔案,用于在寫磁盤的程序出現任何例外可恢復資料;

getxxx() 操作從 SharedPreferencesImpl.mMap 讀取資料.

apply / commit

  • apply 沒有回傳值, commit 有回傳值能知道修改是否提交成功
  • apply 是將修改提交到記憶體,再異步提交到磁盤檔案,而 commit 是同步的提交到磁盤檔案
  • 多并發的提交 commit 時,需等待正在處理的 commit 資料更新到磁盤檔案后才會繼續往下執行,從而降低效率; 而 apply 只是原子更新到記憶體,后呼叫 apply 函式會直接覆寫前面記憶體資料,從一定程度上提高很多效率,

注意

  • 強烈建議不要在 sp 里面存盤特別大的 key/value,有助于減少卡頓 / anr
  • 不要高頻地使用 apply,盡可能地批量提交
  • 不要使用 MODE_MULTI_PROCESS
  • 高頻寫操作的 key 與高頻讀操作的 key 可以適當地拆分檔案,由于減少同步鎖競爭
  • 不要連續多次 edit(),應該獲取一次獲取 edit(),然后多次執行 putxxx(),減少記憶體波動

訊息機制

Handler 機制

Handler 有兩個主要用途:(1)安排 Message 和 runnables 在將來的某個時刻執行; (2)將要在不同于自己的執行緒上執行的操作排入佇列,(在多個執行緒并發更新UI的同時保證執行緒安全,)

Android 規定訪問 UI 只能在主執行緒中進行,因為 Android 的 UI 控制元件不是執行緒安全的,多執行緒并發訪問會導致 UI 控制元件處于不可預期的狀態,為什么系統不對 UI 控制元件的訪問加上鎖機制?缺點有兩個:加鎖會讓 UI 訪問的邏輯變得復雜;其次鎖機制會降低 UI 訪問的效率,如果子執行緒訪問 UI,那么程式就會拋出例外,ViewRootImpl 對UI操作做了驗證,這個驗證作業是由 ViewRootImpl的 checkThread 方法完成:

ViewRootImpl.java

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}
  • Message:Handler 接收和處理的訊息物件
  • MessageQueue:Message 的佇列,先進先出,每一個執行緒最多可以擁有一個
  • Looper:訊息泵,是 MessageQueue 的管理者,會不斷從 MessageQueue 中取出訊息,并將訊息分給對應的 Handler 處理,每個執行緒只有一個 Looper,

Handler 創建的時候會采用當前執行緒的 Looper 來構造訊息回圈系統,需要注意的是,執行緒默認是沒有 Looper 的,直接使用 Handler 會報錯,如果需要使用 Handler 就必須為執行緒創建 Looper,因為默認的 UI 主執行緒,也就是 ActivityThread,ActivityThread 被創建的時候就會初始化 Looper,這也是在主執行緒中默認可以使用 Handler 的原因,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/328037.html

標籤:其他

上一篇:Flutter App啟動流程分析

下一篇:Android 自定義的驗證碼輸入框(無游標),android版本10暫時不支持自定義粘貼

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more