补:《Android面试题思考与解答》12月刊(二)

简介: 日子过的好快,12月又过了,也就代表2020也要结束了。不管你在这一年中是开心,是难过,是苦闷,还是平淡,都过去了,向前看,老铁们~

RxJava的订阅关系


Observable.create(new ObservableOnSubscribe<Integer>() {
        @Override
        public void subscribe(@NonNull ObservableEmitter<Integer> emitter) throws Throwable {
            emitter.onNext(1);
            emitter.onComplete();
        }
    }).subscribe(new Observer<Integer>() {
    @Override
    public void onNext(Integer integer) {
        Log.d(TAG, "onNext: " + integer);
    }
    @Override
    public void onCompleted() {
    }
    @Override
    public void onError(Throwable e) {
        Toast.makeText(activity, "Error!", Toast.LENGTH_SHORT).show();
    }
});


代码中主要有三个角色:


  • 被订阅者Observable,是整个事件的来源,可以发射数据给订阅者。
  • 订阅者Observer,通过subscribe方法和被订阅者产生关系,也就是开始订阅,同时可以接受被订阅者发送的消息。
  • 发射器Subscriber/Emitter,在Rxjava2之后,发射器改为了Emitter,他的作用主要是用来发射一系列事件的,比如next事件,complete事件等等。


有了这三个角色,一个完整的订阅关系也就生成了。


Observer处理完onComplete后会还能onNext吗?


要弄清楚这个问题,得去看看onComplete,onNext方法到底做了什么。


@Override
    public void onComplete() {
        if (!isDisposed()) {
            try {
                 observer.onComplete();
            } finally {
                 dispose();
            }
      }
    }
    @Override
    public void onNext(T t) {
        if (t == null) {
              onError(new NullPointerException("onNext called with null. Null values are generally not allowed in 2.x operators and sources."));
             return;
        }
        if (!isDisposed()) {
             observer.onNext(t);
       }
    }
    public static boolean isDisposed(Disposable d) {
        return d == DISPOSED;
    }
    public static boolean dispose(AtomicReference<Disposable> field) {
        Disposable current = field.get();
        Disposable d = DISPOSED;
        if (current != d) {
            current = field.getAndSet(d);
            if (current != d) {
                if (current != null) {
                    current.dispose();
                }
                return true;
            }
        }
        return false;
    }


源码还是比较清晰明了,无论是onComplete还是onNext,都会判断当前订阅是否被取消,也就是Disposable类型的变量的引用是否等于DISPOSED,如果等于则代表该订阅已经被取消,起点和终点已经断开联系。而在onComplete方法的结尾调用了dispose方法,将原子引用类中的 Disposable 对象设置为 DisposableHelper 内的 DISPOSED 枚举实例,即断开订阅关系,所以在这之后所有的onNext,onComplete,onError方法中的isDisposed判断都不会通过,也就不会执行后续的数据发送等处理了。


RxJava中的操作符


  • concatMap
  • flatMap


这两个操作符的功能是一样的,都是将一个发射数据的Observable变换为多个Observables,然后将它们发射的数据放进一个单独的Observable。区别在于concatMap是有序的,flatMap是无序的,concatMap最终输出的顺序与原序列保持一致,而flatMap则不一定,有可能出现交错。


举个例子,发送数字01234,通过操作符对他们进行+1处理,发送2的时候进行一个延时:


Observable.fromArray(1,2,3,4,5)
                .flatMap(new Function<Integer, ObservableSource<Integer>>() {
                    @Override
                    public ObservableSource<Integer> apply(@NonNull Integer integer) throws Exception {
                        int delay = 0;
                        if(integer == 2){
                            delay = 500;//延迟发射
                        }
                        return Observable.just(integer*10).delay(delay, TimeUnit.MILLISECONDS);
                    }
                })
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<Integer>() {
            @Override
            public void accept(@NonNull Integer integer) throws Exception {
                Log.e("jimu","accept:"+integer);
            }
        });


如上述操作,最终打印结果为:10,20,40,50,30。因为发送数字2的时候,进行了延时。


但是如果flatMap操作符改成concatMap,打印结果就是10,20,30,40,50了,这是因为concatMap是有序的,会按照原序列的顺序进行变换输出。


  • merge、concat、zip,合并


这几个操作符是用作合并发射物的,可以将多个Obserable和并成一个Obserable


Observable<Integer> odds=Observable.just(1,2,3,4);
    Observable<Integer> events=Observable.just(5,6,7,8);
    Observable.merge(odds,events).subscribe(i->Log.d("TAG","merge->"+i));


区别在于concat操作符是在合并后按顺序串行执行,merge操作符是在合并后按时间线并行执行,如果出现某个数据进行延时发射,那么结果序列就会发生变化。


zip操作符的特点是合并之后并行执行,发射事件和最少的一个相同,什么意思呢?比如一个发送两个数据的Obserable和一个发射4条数据的Obserable进行zip合并,那么最终只会有两条数据被发射出来,看个例子:


Observable
   .zip(Observable.just(1,2),Observable.just(3,4,5,6),new BiFunction<Integer, Integer, Integer>() {
            @Override
            public Integer apply(@NonNull Integer response, @NonNull Integer response2) throws Exception {
                //将两个发射器的结果相加
                return response+response2;
            }
        })
        .subscribe(new Consumer<Integer>() {
                    @Override
                    public void accept(@NonNull Integer s) throws Exception {
                        Log.e("lz","accept+"+s);
                    }
                });


结果只会有两条数据:4,6。第二个发射器发射的后面两条数据会被抛弃。


  • interval,周期执行


这个操作符主要用作定时周期任务,比如我需要每100ms发送一次数据:


Observable.interval(100, TimeUnit.MILLISECONDS)
                  .subscribe(new Observer<Long>() {
                    @Override
                    public void onCompleted() {
                    }
                    @Override
                    public void onError(Throwable e) {
                    }
                    @Override
                    public void onNext(Long aLong) {
                    }
                });


  • timer,delay延迟发送数据


这两个操作符都是用作延时发送数据,不同在于timer是创建型操作符,而delay是辅助型操作符。意思就是timer操作符是可以直接创建一个Observable,然后在订阅之后延时发送数据项,看例子:


Observable
  .timer(1000,TimeUnit.MILLISECONDS)
  .subscribeOn(Schedulers.io())
  .subscribe(disposableObserver);


delay是当原始的Observable发送数据后,启动一个定时器,然后延时将这个数据发送,所以它相当于是处在上游与下游之间的一个辅助项,用作延时发送,它的作用对象必须是个创建好的Observable


Observable
  .just(0L)
  .doOnNext(new Consumer<Long>() {
            @Override
            public void accept(Long aLong) throws Exception {
            }
        }
  .timer(1000,TimeUnit.MILLISECONDS)
  .subscribeOn(Schedulers.io())
  .subscribe(disposableObserver);


Drawable、Canvas、Bitmap


Drawable表示的是一种可以在Canvas上进行绘制的抽象的概念,种类很多,最常见的颜色和图片都可以是一个Drawable。


所以他是一种抽象的概念,是表示一种图像,但是又不全是图片,也可以表示颜色,一般被用作View的背景或者填充图。


到这里有的小伙伴可能又要问了,Canvas又是什么呢?


Canvas一个矩形区域, 没有边界, 我们可以在上面调用任意drawXXX开头的方法绘制到引用的Bitmap上. 同时提供对图形的处理, 比如裁剪, 缩放, 旋转(需要Matrix对象配合使用). 所以Canvas与其说是画布, 不如说是一个绘制工具,它实际操作和存储的像素数据都在它的私有成员 Bitmap 对象中,所谓的画到Canvas画布上,其实就是画到其Bitmap,存储到其Bitmap变量中。


说到这,又要提下Bitmap了:


Bitmap是一个存储格式, 存储了一个矩形区域内各像素点的信息. 这种格式适合显示, 但是存储效率低.可以理解为int[] buffer,用来保存每个像素的信息。


所以这三者的关系简单点就是:


Drawable表示一个可以被绘制的图像,是一个抽象概念,需要通过Canvas绘制到Bitmap上,作为一个图像的存储。所以Bitmap是Drawable存在的一种实体。


Drawable分类


Drawable的种类很多,这里列举几个比较常用的,具体代码使用方式可以看往期文章这次来把Drawable翻了个遍


  • BitmapDrawablw 最常用的Drawable,表示的就是一张图片,一张带规则的图片,可以设置一些规则,比较适用于对图片有限制的情况,比如背景图。
  • NinePatchDrawable 和BitmapDrawablw类似,使用时src传.9图片即可。实际上BitmapDrawablw也可以直接用.9图片,所以这个NinePatchDrawable没有太大的实际作用。
  • ShapeDrawable 这个很常见,一般纯颜色的图形就用这个画出来。
  • LayerDrawable


这是一种层次化的Drawable,相当于一种Drawable的布局嵌套,或者说集合,通过不同的Drawable放置到不同的层上达到一种叠加后的效果。


  • StateListDrawble


一般用作点击效果,对应标签是selector,可以设置按下,点击后,不点击时候的各种状态。


  • LevelListDrawable


和LayerDrawable类似,也是一个Drawable的集合,但是他是有等级的概念的,也就是可以通过设置不同的等级来展示对应的drawable,有点像一个有多种样式的Drawable,可以通过代码来展示哪一个样式。可以通过ImageView的setImageLevel方法来切换Drawable。


  • TransitionDrawable


该Drawable可以实现两个Drawable之间的淡入淡出,对应的标签是transition。


  • InsetDrawable


可以将其他的Drawable内嵌到自己当中,当一个View希望自己的背景比自己的实际区域小的时候,就可以采用这个,通过LayerDrawable也可以实现。一般用作加大点击区域。


  • ScaleDrawable


用于缩放,并且和它的等级Level有关,等级为0则不可见。


  • ClipDrawable


用作裁剪,会根据自己的等级来裁剪一个Drawable,等级0表示完全裁剪,即整个Drawable都不可见,等级10000表示不裁剪,所以主要是通过控制drawable的等级来完成裁剪功能


Window是什么?在Android中都用到了哪些地方


  • 首先,它是一个窗口,是Android中唯一的展示视图的中介,所有的视图都是通过Window来呈现的,无论是Activity,Dialog或Toast,他们的视图都是附加到WIndow上的,所以Window是View的直接管理者。
  • Window是一个抽象类,他的具体实现就是PhoneWindow。
  • Window的具体实现在WindowManagerService中,但是创建Window或者访问Window的操作都需要WindowManager。所以这就需要WindowManager和WindowManagerService进行交互,交互的方式就是通过IPC,具体涉及的参数就是token。
  • 每一个Window都对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl建立联系,所以Window并不是实际存在的,而是以View的形式存在。


涉及到Window的地方:


  • 事件分发机制。界面上事件分发机制的开始都是这样一个过程:DecorView——>Activity——>PhoneWindow——>DecorView——>ViewGroup。之前看过一个比较有趣的问题:事件到底是先到DecorView还是先到Window的?,其实是先到DecorView的,具体逻辑可以自己翻下源码,有机会也可以出篇文章讲讲~
  • 各种视图的显示。比如Activity的setContentView,Dialog,Toast的显示视图等等都是通过Window完成的。


Window的分层和类别?


  • 由于界面上有不止一个的Window,所以就有了分层的概念。每个Window都有自己对应的Window层级—z-ordered,层级大的会覆盖到层级小的上面,类似HTML中的z-index。


Window主要分为三个类别:


  • 应用Window。对应着一个Activity,Window层级为1-99,在视图最下层。
  • 子Window。不能单独存在,需要附属在特定的父Window之中(如Dialog就是子Window),Window层级为1000~1999。
  • 系统Window。需要声明权限才能创建的Window,比如Toast和系统状态栏,Window层级为2000~2999,处在视图最上层。


Window的内部机制—添加、删除、更新。


  • 之前说过,Window的操作都是通过WindowManager来完成的,而WindowManager是一个接口,他的实现类是WindowManagerImpl,并且全部交给WindowManagerGlobal来处理。下面具体说下addView,updateViewLayout,和removeView。


1) addView


public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
      //...
        ViewRootImpl root;
        View panelParentView = null;
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
            try {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }


  • 首先,通过add方法修改了WindowManagerGlobal中的一些参数,比如mViews—存储了所有Window所对应的View,mRoots——所有Window所对应的ViewRootImpl,mParams—所有Window对应的布局参数。
  • 其次,setView方法主要完成了两件事,一是通过requestLayout方法完成异步刷新界面的请求,进行完整的view绘制流程。其次,会通过WindowSession进行一次IPC调用,交给到WMS来实现Window的添加。


2)updateViewLayout


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


这里更新了WindowManager.LayoutParamsViewRootImpl.LayoutParams,然后在ViewRootImpl内部同样会重新对View进行绘制,最后通过IPC通信,调用到WMS的relayoutWindow完成更新。


3)removeView


public void removeView(View view, boolean immediate) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }
        synchronized (mLock) {
            int index = findViewLocked(view, true);
            View curView = mRoots.get(index).getView();
            removeViewLocked(index, immediate);
            if (curView == view) {
                return;
            }
            throw new IllegalStateException("Calling with view " + view
                    + " but the ViewAncestor is attached to " + curView);
        }
    }
    private void removeViewLocked(int index, boolean immediate) {
        ViewRootImpl root = mRoots.get(index);
        View view = root.getView();
        if (view != null) {
            InputMethodManager imm = view.getContext().getSystemService(InputMethodManager.class);
            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);
            }
        }
    }


该方法中,通过view找到mRoots中的对应索引,然后同样走到ViewRootImpl中进行View删除工作,通过die方法,最终走到dispatchDetachedFromWindow()方法中,主要做了以下几件事:


  • 回调onDetachedFromeWindow。
  • 垃圾回收相关操作;
  • 通过Session的remove()在WMS中删除Window;
  • 通过Choreographer移除监听器


Window中的Token是什么?


public abstract class Window {
    private IBinder mAppToken;
}


  • 是Window类中的一个变量,是一个Binder对象。在Window中主要是实现WindowManagerService和应用所在的进程通信,也就是上文说到的WindowManager和WindowManagerService进行交互。
  • 是一个添加view的权限标识。拥有token的context可以创建界面、进行UI操作,而没有token的context如service、Application,是不允许添加view到屏幕上的。所以它存在的意义就是为了保护window的创建,也是为了防止Application或Service来做进行view或者UI相关的一些操作。


Activity,Dialog,Toast的Window创建过程


上篇文章说过Dialog的创建,先来回顾下:


1)Dialog


//构造函数
Dialog(Context context, int theme, boolean createContextThemeWrapper) {
        //......
        //获取了WindowManager对象,mContext一般是个Activity,获取系统服务一般是通过Binder获取
        mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
        //创建新的Window
        Window w = PolicyManager.makeNewWindow(mContext);
        mWindow = w;
        //这里也是上方mWindow.getCallback()为什么是Activity的原因,在创建新Window的时候会设置callback为自己
        w.setCallback(this);
        w.setOnWindowDismissedCallback(this);
        //关联WindowManager与新Window,token为null
        w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);
        mListenersHandler = new ListenersHandler(this);
    }
//show方法
    public void show() {
        //......
        if (!mCreated) {
            //回调Dialog的onCreate方法
            dispatchOnCreate(null);
        }
        //回调Dialog的onStart方法
        onStart();
        //获取当前新Window的DecorView对象
        mDecor = mWindow.getDecorView();
        WindowManager.LayoutParams l = mWindow.getAttributes();
        try {
            //把一个View添加到Activity共用的windowManager里面去
            mWindowManager.addView(mDecor, l);
            //......
        } finally {
        }
    }


可以看到一个Dialog从无到有经历了以下几个步骤:


  • 首先创建了一个新的Window,类型是PhoneWindow类型,与Activity创建Window过程类似,并设置setCallback回调。
  • 将这个新Window与从Activity拿到的WindowManager对象相关联,也就是dialog与Activity公用了同一个WindowManager对象。
  • show方法展示Dialog,先回调了Dialog的onCreate,onStart方法。
  • 然后获取Dialog自己的DecorView对象,并通过addView方法添加到WindowManager对象中,Dialog出现到屏幕上。


2)Activity


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, IBinder assistToken) {
        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);
        }
        mUiThread = Thread.currentThread();
        //...
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        if (mParent != null) {
            mWindow.setContainer(mParent.getWindow());
        }
        mWindowManager = mWindow.getWindowManager();
        mCurrentConfig = config;
        mWindow.setColorMode(info.colorMode);
        setAutofillOptions(application.getAutofillOptions());
        setContentCaptureOptions(application.getContentCaptureOptions());
    }
    public void setContentView(@LayoutRes int layoutResID) {
        // 交给Window
        getWindow().setContentView(layoutResID);
        // 创建ActionBar
        initWindowDecorActionBar();
    }


关于Activity的启动流程,相比大伙都知道些,流程最后会走到ActivityThread中的performLauchActivity方法,然后会创建Activity的实例对象,并调用attach方法,也就是上述贴的源码。


在这个方法中,创建了新的Window对象,设置回调接口。这个回调接口主要就是用作Window在接收到外界状态改变的时候,就会回调给这个callback,比如onAttachedToWindow、dispatchTouchEvent方法等,这个上篇文章也有说过,事件分发的时候就是通过在DecorView中这个callback进行分发的。


然后view怎么显示到界面上的呢,Activity可没有show方法哦?其实就是通过setContentView方法。该方法主要做了以下几件事:


  • 创建DecorView,如果不存在的话。
  • 然后将xml中解析到的view添加到DecorView的mContentParent中,也就是布局为android.R.id.content的ContentView。
  • 回调onContentChanged方法,通知Activity视图已经发生改变。


贴张图:


11.png


到这里,一个有完整view结构的DecorView就创建出来了,但是它还没有被显示到手机界面上,也就是没有被添加到Window中。最后要调用了WMS的addView方法才会被用户真正看到:


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


3)Toast


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;
        final int displayId = mContext.getDisplayId();
        try {
            service.enqueueToast(pkg, tn, mDuration, displayId);
        } catch (RemoteException e) {
            // Empty
        }
    }
    public void cancel() {
        mTN.cancel();
    }
//class TN
    public void handleShow() {
       // ......
        mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
        mWM.addView(mView, mParams);
    }
    public void handleHide() {
        if (mView != null) {
            if (mView.getParent() != null) {
                mWM.removeView(mView);
            }
            mView = null;
        }
    }


Toast有点不同的在于,它内部维护了两个IPC通信,一个是NotificationManagerService,一个是回调TN接口。最终的实现都是走到TN.class的handleShowhandleHide方法,也就是addView和removeView。


Android中创建多进程的方式


1) 第一种,大家熟知的,就是给四大组件再AndroidManifest中指定android:process属性。


<activity android:name="com.example.uithread.UIActivity" 
      android:process=":test"/>
   <activity android:name="com.example.uithread.UIActivity2"
      android:process="com.example.test"/>


可以看到,android:process有两种表达方式:


  • :test。“:”的含义是指要在当前的进程名前面加上当前的包名,如果当前包名为com.example.jimu。那么这个进程名就应该是com.example.jimu:test。这种冒号开头的进程属于当前应用的私有进程,其他应用的组件不可以和他跑到同一个进程中。
  • com.example.test。第二种表达方式,是完整的命名方式,它就是新进程的进程名,这种属于全局进程,其他应用可以通过shareUID的方式跑到同一个进程中。


简单说下shareUID:正常来说,Android中每个app都是一个单独的进程,与之对应的是一个唯一的linux user ID,所以就能保住该应用程序的文件或者组件只对该应用程序可见。但是也有一个办法能让不同的apk进行共享文件,那就是通过shareUID,它可以使不同的apk使用相同的 user ID。贴下用法:


//app1
<manifest package="com.test.app1"
android:sharedUserId="com.test.jimu"
>
//app2
<manifest package="com.test.app2"
android:sharedUserId="com.test.jimu"
>
//app1中获取app2的上下文:
Context mContext=this.createPackageContext("com.test.app2", Context.CONTEXT_IGNORE_SECURITY);


2)第二种创建进程的方法,就是通过JNI在native层中去fork一个进程。


这种就比较复杂了,我在网上找了一些资料,找到一个fork普通进程的:


//主要代码
long add(long x,long y)
{
   //fpid表示fork函数返回的值 
    pid_t fpid; 
    int count=0; 
    fpid=fork();  
}
//结果:
USER       PID   PPID   VSZ     RSS  STAT  NAME                 
root       152  1              S    zygote
u0_a66   17247  152   297120  44096  S  com.example.jni
u0_a66   17520  17247  0    0    Z  com.example.jni


最终的结果是可以创建出一个进程,但是没有运行,占用的内存为0,处于僵尸程序状态。


但是它这个是通过普通进程fork出来的,我们知道Android中所有的进程都是直接通过zygote进程fork出来的(fork可以理解为孵化出来的当前进程的一个副本)。所以不知道直接去操作zygote进程可不可以成功,有了解的小伙伴可以在微信讨论群里给大家说说。


对了,有的小伙伴可能会问,为什么所有进程都必须用zygote进程fork呢?


  • 这是因为fork的行为是复制整个用户的空间数据以及所有的系统对象,并且只复制当前所在的线程到新的进程中。也就是说,父进程中的其他进程在子进程中都消失了,为了防止出现各种问题(比如死锁,状态不一致)呢,就只让zygote进程,这个单线程的进程,来fork新进程。
  • 而且在zygote进程中会做好一些初始化工作,比如启动虚拟机,加载系统资源。这样子进程fork的时候也就能直接共享,提高效率,这也是这种机制的优点。


一个应用使用多进程会有什么问题吗?


上面说到创建进程的方法很简单,写个android:process属性即可,那么使用是不是也这么简单呢?很显然不是,一个应用中多进程会导致各种各样的问题,主要有如下几个:


  • 静态成员和单例模式完全失效。因为每个进程都会分配到一个独立的虚拟机,而不同的虚拟机在内存分配上有不同的地址空间,所以在不同的进程,也就是不同的虚拟机中访问同一个类的对象会产生多个副本。
  • 线程同步机制完全失效。同上面一样,不同的内存是无法保证线程同步的,因为线程锁的对象都不一样了。
  • SharedPreferences不在可靠。之前有一篇说SharedPreferences的文章中说过这一点,SharedPreferences是不支持多进程的。
  • Application会多次创建。多进程其实就对应了多应用,所以新进程创建的过程其实就是启动了一个新的应用,自然也会创建新的Application,Application和虚拟机和一个进程中的组件是一一对应的。


Android中的IPC方式


既然多进程有很多问题,自然也就有解决的办法,虽然不能共享内存,但是可以进行数据交互啊,也就是可以进行多进程间通信,简称IPC。


下面就具体说说Android中的八大IPC方式:


  • Bundle Android四大组件都是支持在Intent中使用Bundle来传递数据,所以四大组件直接的进程间通信就可以使用Bundle。但是Bundle有个大小限制要注意下,bundle的数据传递限制大小为1M,如果你的数据超过这个大小就要使用其他的通信方式了。
  • 文件共享 这种方式就是多个进程通过读写一个文件来交换数据,完成进程间通信。但是这种方式有个很大的弊端就是多线程读写容易出问题,也就是并发问题,如果出现并发读或者并发写都容易出问题,所以这个方法适合对数据同步要求不高的进程直接进行通信。


这里可能有人就奇怪了,SharedPreference不就是读写xml文件吗?怎么就不支持进程间通信了?


  • 这是因为系统对于SharedPreference有读写缓存策略,也就是在内存中有一份SharedPreference文件的缓存,涉及到内存了,那肯定在多进程中就不那么可靠了。
  • MessengerMessenger是用来传递Message对象的,在Message中可以放入我们要传递的数据。它是一种轻量级的IPC方案,底层实现是AIDL。
  • AIDL


Messenger虽然可以发送消息和接收消息,但是无法同时处理大量消息,并且无法跨进程方法。但是AIDL则可以做到,这里简单说下AIDL的使用流程:


服务端首先建立一个Service监听客户端的连接请求,然后创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中申明,最后在Service中实现这个AIDL接口。客户端需要绑定这个服务端的Service,然后将服务端返回的Binder对象转换成AIDL接口的属性,然后就可以调用AIDL中的方法了。


  • ContentProvider


这个大家应很熟悉了,四大组件之一,专门用于不同应用间进行数据共享的。它的底层实现是通过Binder实现的。


  • Socket


套接字,在网络通信中用的很多,比如TCP,UDP。关于Socket通信,借用网络上的一张图说明:


10.png


  • Binder连接池


关于Binder的介绍,之前的文章已经说过了。这里主要讲一个Binder的实际使用的技术——Binder连接池。由于每个AIDL请求都要开启一个服务,防止太多服务被创建,就引用了Binder连接池技术。Binder连接池的主要作用就是将每个业务模块的Binder请求统一 转发到远程Service中去执行,从而避免了重复创建Service的过程。贴一下Binder连接池的工作原理:


9.png


  • 每个业务模块创建自己的AIDL接口并实现此接口,然后向服务端提供自己的唯一标识和其对应的Binder对象.
  • 对于服务端来说,只需要一个 Service就可以了,服务端提供一个queryBinder接口,这个接口能够根据业务模块的特征来 返回相应的Binder对象给它们,不同的业务模块拿到所需的Binder对象后就可以进行远程方法调用了。


  • BroadcastReceiver


广播,不用多说了吧~ 像我们可以监听系统的开机广播,网络变动广播等等,都是体现了进程间通信的作用。


Android在版本迭代中,总会进行很多改动,那么你熟知各版本都改动了什么内容?又要怎么适配呢?


这个内容太多,我就不贴了哈,具体可以往期文章 Android版本迭代信息


冷启动、温启动、热启动


首先了解下启动的这三个概念,也是面试常被问到的:


  • 冷启动。冷启动指的是该应用程序在此之前没有被创建,发生在应用程序首次启动或者自上次被终止后的再次启动。简单的说就是app进程还没有,需要创建app的进程启动app。


比如开机后,点击屏幕的app图标启动应用。


冷启动的过程主要分为两步:


1)系统任务。加载并启动应用程序;显示应用程序的空白启动窗口;创建APP进程 2)APP进程任务。启动主线程;创建Activity;加载布局;屏幕布局;绘制屏幕


其实这不就是APP的启动流程嘛?所以冷启动是会完整走完一个启动流程的,从系统到进程。


  • 温启动。温启动指的是App进程存在,但Activity可能因为内存不足被回收,这时候启动App不需要重新创建进程,只需要执行APP进程中的一些任务,比如创建Activity。


比如返回主页后,又继续使用其他的APP,时间久了或者打开的应用多了,之前应用的Activity有可能被回收了,但是进程还在。


所以温启动相当于执行了冷启动的第二过程,也就是APP进程任务,需要重新启动线程,Activity等。


  • 热启动。热启动就是App进程存在,并且Activity对象仍然存在内存中没有被回收。


比如app被切到后台,再次启动app的过程。


所以热启动的开销最少,这个过程只会把Activity从后台展示到前台,无需初始化,布局绘制等工作。


启动优化我们可以介入的优化点


所以三种启动方式中,冷启动经历的时间最长,也是走完了最完整的启动流程,所以我们再次分析下冷启动的启动流程,看看有哪些可以优化的点:


  • Launcher startActivity
  • AMS startActivity
  • Zygote fork 进程
  • ActivityThread main()
  • ActivityThread attach
  • handleBindApplication
  • attachBaseContext
  • Application attach
  • installContentProviders
  • Application onCreate
  • Looper.loop
  • Activity onCreate,onResume


纵观整个流程,其实我们能动的地方不多,无非就是Application的attach,onCreate方法,Activity的onCreate,onResume方法,这些方法也就是我们的优化点。


启动优化方案总结


最后再和大家回顾下今天说到的启动优化方案:


  • 消除启动时的白屏/黑屏。windowBackground。
  • 第三方库懒加载/异步加载。线程池,启动器。
  • 预创建Activity。对象预创建。
  • 预加载数据。
  • Multidex预加载优化。5.0以下多dex情况。
  • Webview启动优化。预创建,缓存池,静态资源。
  • 避免布局嵌套。多层嵌套。


为了方便记忆,我再整理成以下三类,分别是Application、Activity、UI


  • Application 三方库,Multidex。
  • Activity 预创建类,预加载数据。
  • UI方面 windowBackground,布局嵌套,webview。


具体说明可以看往期文章 Android启动优化全解析


目录
相关文章
|
4月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
**Kotlin中的`by lazy`和`lateinit`都是延迟初始化技术。`by lazy`用于只读属性,线程安全,首次访问时初始化;`lateinit`用于可变属性,需手动初始化,非线程安全。`by lazy`支持线程安全模式选择,而`lateinit`适用于构造函数后初始化。选择依赖于属性特性和使用场景。**
155 5
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
|
4月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin中常见作用域函数
**Kotlin作用域函数概览**: `let`, `run`, `with`, `apply`, `also`. `let`安全调用并返回结果; `run`在上下文中执行代码并返回结果; `with`执行代码块,返回结果; `apply`配置对象后返回自身; `also`附加操作后返回自身
61 8
|
4月前
|
Android开发 开发者
Android经典面试题之SurfaceView和TextureView有什么区别?
分享了`SurfaceView`和`TextureView`在Android中的角色。`SurfaceView`适于视频/游戏,独立窗口低延迟,但变换受限;`TextureView`支持复杂变换,视图层级中渲染,适合动画/视频特效,但性能略低。两者在性能、变换、使用和层级上有差异,开发者需按需选择。
79 1
|
4月前
|
SQL 安全 Java
Android经典面试题之Kotlin中object关键字实现的是什么类型的单例模式?原理是什么?怎么实现双重检验锁单例模式?
Kotlin 单例模式概览 在 Kotlin 中,`object` 关键字轻松实现单例,提供线程安全的“饿汉式”单例。例如: 要延迟初始化,可使用 `companion object` 和 `lazy` 委托: 对于参数化的线程安全单例,结合 `@Volatile` 和 `synchronized`
63 6
|
4月前
|
SQL Java Unix
Android经典面试题之Java中获取时间戳的方式有哪些?有什么区别?
在Java中获取时间戳有多种方式,包括`System.currentTimeMillis()`(毫秒级,适用于日志和计时)、`System.nanoTime()`(纳秒级,高精度计时)、`Instant.now().toEpochMilli()`(毫秒级,ISO-8601标准)和`Instant.now().getEpochSecond()`(秒级)。`Timestamp.valueOf(LocalDateTime.now()).getTime()`适用于数据库操作。选择方法取决于精度、用途和时间起点的需求。
67 3
|
4月前
|
XML Android开发 数据格式
Android面试题之DialogFragment中隐藏导航栏
在Android中展示全屏`DialogFragment`并隐藏状态栏和导航栏,可通过设置系统UI标志实现。 记得在布局文件中添加内容,并使用`show()`方法显示`DialogFragment`。
63 2
|
4月前
|
Android开发
Android面试题之View的invalidate方法和postInvalidate方法有什么区别
本文探讨了Android自定义View中`invalidate()`和`postInvalidate()`的区别。`invalidate()`在UI线程中刷新View,而`postInvalidate()`用于非UI线程,通过消息机制切换到UI线程执行`invalidate()`。源码分析显示,`postInvalidate()`最终调用`ViewRootImpl`的`dispatchInvalidateDelayed`,通过Handler发送消息到UI线程执行刷新。
58 1
|
4月前
|
消息中间件 调度 Android开发
Android经典面试题之View的post方法和Handler的post方法有什么区别?
本文对比了Android开发中`View.post`与`Handler.post`的使用。`View.post`将任务加入视图关联的消息队列,在视图布局后执行,适合视图操作。`Handler.post`更通用,可调度至特定Handler的线程,不仅限于视图任务。选择方法取决于具体需求和上下文。
53 0
|
4月前
|
Android开发 Kotlin
Android经典面试题之Kotlin中Lambda表达式有哪些用法
Kotlin的Lambda表达式是匿名函数的简洁形式,常用于集合操作和高阶函数。基本语法是`{参数 -&gt; 表达式}`。例如,`{a, b -&gt; a + b}`是一个加法lambda。它们可在`map`、`filter`等函数中使用,也可作为参数传递。单参数时可使用`it`关键字,如`list.map { it * 2 }`。类型推断简化了类型声明。
28 0
|
4月前
|
Android开发 Kotlin
Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
**Kotlin中的匿名函数与Lambda表达式概述:** 匿名函数(`fun`关键字,明确返回类型,支持非局部返回)适合复杂逻辑,而Lambda(简洁语法,类型推断)常用于内联操作和高阶函数参数。两者在语法、返回类型和使用场景上有所区别,但都提供无名函数的能力。
35 0
下一篇
无影云桌面