个人认为重要的Android面试总结六

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

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视图已经发生改变。

贴张图:

到这里,一个有完整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的handleShow和handleHide方法,也就是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是不支持多进程的。
  • Application会多次创建。多进程其实就对应了多应用,所以新进程创建的过程其实就是启动了一个新的应用,自然也会创建新的Application,Application和虚拟机和一个进程中的组件是一一对应的。

Android中的IPC方式(多进程间通信,简称IPC

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

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

1.)Bundle Android四大组件都是支持在Intent中使用Bundle来传递数据,所以四大组件直接的进程间通信就可以使用Bundle。但是Bundle有个大小限制要注意下,bundle的数据传递限制大小为1M,如果你的数据超过这个大小就要使用其他的通信方式了。

2.)文件共享 这种方式就是多个进程通过读写一个文件来交换数据,完成进程间通信。但是这种方式有个很大的弊端就是多线程读写容易出问题,也就是并发问题,如果出现并发读或者并发写都容易出问题,所以这个方法适合对数据同步要求不高的进程直接进行通信

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

  • 这是因为系统对于SharedPreference有读写缓存策略,也就是在内存中有一份SharedPreference文件的缓存,涉及到内存了,那肯定在多进程中就不那么可靠了

3.)MessengerMessenger是用来传递Message对象的,在Message中可以放入我们要传递的数据。它是一种轻量级的IPC方案,底层实现是AIDL。

4.)AIDL

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

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

5.)ContentProvider

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

6.)Socket

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

7.)Binder连接池 

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

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

8.)BroadcastReceiver 

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

冷启动、温启动、热启动

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

  • 冷启动。冷启动指的是该应用程序在此之前没有被创建,发生在应用程序首次启动或者自上次被终止后的再次启动。简单的说就是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启动优化全解析:https://mp.weixin.qq.com/s?__biz=MzU0MTYwMTIzMw==&mid=2247484835&idx=1&sn=f62c8fb5c9a461987f5bb5446bc5a19b&scene=21#wechat_redirect

Bitmap是什么,怎么存储图片。

Bitmap,位图,本质上是一张图片的内容在内存中的表达形式。它将图片的内容看做是由存储数据的有限个像素点组成;每个像素点存储该像素点位置的ARGB值,每个像素点的ARGB值确定下来,这张图片的内容就相应地确定下来。其中,A代表透明度,RGB代表红绿蓝三种颜色通道值。

Bitmap内存如何计算

Bitmap一直都是Android中的内存大户,计算大小的方式有三种:

  • getRowBytes() 这个在API Level 1添加的,返回的是bitmap一行所占的大小,需要乘以bitmap的高,才能得出btimap的大小
  • getByteCount() 这个是在API Level 12添加的,其实是对getRowBytes()乘以高的封装
  • getAllocationByteCount() 这个是在API Level 19添加的

这里我将一张图片放到项目的drawable-xxhdpi文件夹中,然后通过方法获取图片所占的内存大小:

var bitmap = BitmapFactory.decodeResource(resources, R.drawable.test)
    img.setImageBitmap(bitmap)

    Log.e(TAG,"dpi = ${resources.displayMetrics.densityDpi}")
    Log.e(TAG,"size = ${bitmap.allocationByteCount}")

打印出来的结果是

size=1960000

具体是怎么计算的呢?

图片内存=宽 * 高 * 每个像素所占字节。

这个像素所占字节又和Bitmap.Config有关,Bitmap.Config是个枚举类,用于描述每个像素点的信息,比如:

  • ARGB_8888。常用类型,总共32位,4个字节,分别表示透明度和RGB通道。
  • RGB_565。16位,2个字节,只能描述RGB通道。

所以我们这里的图片内存计算就得出:

宽700 * 高700 * 每个像素4字节=1960000

Bitmap内存 和drawable目录的关系

首先放一张drawable目录对应的屏幕密度对照表,来自郭霖的博客:

刚才的案例,我们是把图片放到drawable-xxhdpi文件夹,而drawable-xxhdpi文件夹对应的dpi就是我们测试手机的dpi—480。所以图片的内存就是我们所计算的宽 * 高 * 每个像素所占字节。

如果我们把图片放到其他的文件夹,比如drawable-hdpi文件夹(对应的dpi是240),会发生什么呢?

再次打印结果:

size = 7840000

这是因为一张图片的实际占用内存大小计算公式是:

占用内存 = 宽 * 缩放比例 * 高 * 缩放比例 * 每个像素所占字节

这个缩放比例就跟屏幕密度DPI有关了:

缩放比例 = 设备dpi/图片所在目录的dpi

所以我们这张图片的实际占用内存位:

宽700 * (480/240) * 高700 * (480/240) * 每个像素4字节 = 7840000

Bitmap加载优化?不改变图片质量的情况下怎么优化?

常用的优化方式是两种:

  • 修改Bitmap.Config

这一点刚才也说过,不同的Conifg代表每个像素不同的占用空间,所以如果我们把默认的ARGB_8888改成RGB_565,那么每个像素占用空间就会由4字节变成2字节了,那么图片所占内存就会减半了。

可能一定程度上会降低图片质量,但是我实际测试看不出什么变化。

  • 修改inSampleSize

inSampleSize,采样率,这个参数是用于图片尺寸压缩的,他会在宽高的维度上每隔inSampleSize个像素进行一次采集,从而达到缩放图片的效果。这种方法只会改变图片大小,不会影响图片质量。

val options=BitmapFactory.Options()
    options.inSampleSize=2
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.test2,options)
    img.setImageBitmap(bitmap)

实际项目中,我们可以设置一个与目标图像大小相近的inSampleSize,来减少实际使用的内存:

fun getImage(): Bitmap {
        var options = BitmapFactory.Options()
        options.inJustDecodeBounds = true
        BitmapFactory.decodeResource(resources, R.drawable.test2, options)
        // 计算最佳采样率
        options.inSampleSize = getImageSampleSize(options.outWidth, options.outHeight)
        options.inJustDecodeBounds = false
        return BitmapFactory.decodeResource(resources, R.drawable.test2, options)
    }

inJustDecodeBounds是什么?

上面的例子大家应该发现了,其中有个inJustDecodeBounds,又设置为true,又设置成false的,总感觉多此一举,那么他到底是干嘛呢?

因为我们要获取图片本身的大小,如果直接decodeResource加载一遍的话,那么就会增加内存了,所以官方提供了这样一个参数inJustDecodeBounds。如果inJustDecodeBounds为ture,那么decode的bitmap为null,也就是不返回实际的bitmap,只把图片的大小信息放到了options的值中。

所以这个参数就是用来获取图片的大小信息的同时不占用内存。

Bitmap内存复用怎么实现?

如果有个需求,是在同一个imageview中可以加载不同的图片,那我们需要每次都去新建一个Bitmap对象,占用新的内存空间吗?如果我们这样写的话:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.actvitiy_bitmap)

        btn1.setOnClickListener {
            img.setImageBitmap(getBitmap(R.drawable.test))
        }

        btn2.setOnClickListener {
            img.setImageBitmap(getBitmap(R.drawable.test2))
        }
    }

fun getBitmap(resId: Int): Bitmap {
    var options = BitmapFactory.Options()
    return BitmapFactory.decodeResource(resources, resId, options)
} 

 这样就会Bitmap就会频繁去申请内存,释放内存,从而导致大量GC,内存抖动。

为了防止这种情况呢,我们就可以用到inBitmap参数,用于Bitmap的内存复用。这样同一块内存空间就可以被多个Bitmap对象复用,从而减少了频繁的GC。

val options by lazy {
    BitmapFactory.Options()
}

val reuseBitmap by lazy {
    options.inMutable = true
    BitmapFactory.decodeResource(resources, R.drawable.test, options)
}

fun getBitmap(resId: Int): Bitmap {
    options.inMutable = true
    options.inBitmap = reuseBitmap
    return BitmapFactory.decodeResource(resources, resId, options)
}

这里有几个要注意的点

  • inBitmap要和inMutable属性配套使用,否则将无法复用。
  • 在Android 4.4之前,只能重用相同大小的 Bitmap 内存区域;
  • 4.4之后只要复用内存空间的Bitmap对象大小比inBitmap指向的内存空间要小即可。

所以一般在复用之前,还要判断下,新的Bitmap内存是不是小于可以复用的Bitmap内存,然后才能进行复用。

高清大图加载该怎么处理?

如果是高清大图,那就说明不允许进行图片压缩,比如微博长图,清明上河图。

所以我们就要对图片进行局部显示,这就用到BitmapRegionDecoder属性,主要用于显示图片的某一块矩形区域。

比如我要显示左上角的100 * 100区域:

fun setImagePart() {
    val inputStream: InputStream = assets.open("test.jpg")
    val bitmapRegionDecoder: BitmapRegionDecoder =
        BitmapRegionDecoder.newInstance(inputStream, false)
    val options = BitmapFactory.Options()
    val bitmap = bitmapRegionDecoder.decodeRegion(Rect(0, 0, 100, 100), options)
    image.setImageBitmap(bitmap)
}

实际项目使用中,我们可以根据手势滑动,然后不断更新我们的Rect参数来实现具体的功能即可。

具体实现源码可以参考鸿洋的博客:https://blog.csdn.net/lmj623565791/article/details/49300989

如何跨进程传递大图?

  • Bundle直接传递。bundle最常用于Activity间传递,也属于跨进程的一种方式,但是传递的大小有限制,一般为1M。
//intent.put的putExtra方法实质也是通过bundle
intent.putExtra("image",bitmap);

bundle.putParcelable("image",bitmap)

Bitmap之所以可以直接传递,是因为其实现了Parcelable接口进行了序列化。而Parcelable的传递原理是利用了Binder机制,将Parcel序列化的数据写入到一个共享内存(缓冲区)中,读取的时候也会从这个缓冲区中去读取字节流,然后再反序列化成对象使用。这个共享内存也就是缓存区有一个大小限制—1M,而且是公用的。所以传图片的话很容易就容易超过这个大小然后报错TransactionTooLargeException。

所以这个方案不可靠。

  • 文件传输。

将图片保存到文件,然后只传输文件路径,这样肯定是可以的,但是不高效。

  • putBinder

这个就是考点了。通过传递binder的方式传递bitmap。

//传递binder
val bundle = Bundle()
bundle.putBinder("bitmap", BitmapBinder(mBitmap))

//接收binder中的bitmap
val imageBinder: BitmapBinder = bundle.getBinder("bitmap") as BitmapBinder
val bitmap: Bitmap? = imageBinder.getBitmap()

//Binder子类
class BitmapBinder :Binder(){
    private var bitmap: Bitmap? = null

    fun ImageBinder(bitmap: Bitmap?) {
        this.bitmap = bitmap
    }

    fun getBitmap(): Bitmap? {
        return bitmap
    }
}

为什么用putBinder就没有大小限制了呢?

  • 因为putBinder中传递的其实是一个文件描述符fd,文件本身被放到一个共享内存中,然后获取到这个fd之后,只需要从共享内存中取出Bitmap数据即可,这样传输就很高效了。
  • 而用Intent/bundle直接传输的时候,会禁用文件描述符fd,只能在parcel的缓存区中分配空间来保存数据,所以无法突破1M的大小限制。

文件描述符是一个简单的整数,用以标明每一个被进程所打开的文件和socket。第一个打开的文件是0,第二个是1,依此类推。

描述new一个对象的过程

先上图,再描述:

  

Java中对象的创建过程包括 类初始化类实例化两个阶段。而new就是创建对象的一种方式,一种时机。

当执行到new的字节码指令的时候,会先判断这个类是否已经初始化,如果没有初始化就要进行类的初始化,也就是执行类构造器<clinit>()方法。如果已经初始化了,就直接进行类对象的实例化。

  • 类的初始化,是类的生命周期中的一个阶段,会为类中各个类成员赋初始值。
  • 类的实例化,是指创建一个类的实例的过程。

但是在类的初始化之前,JVM会保证类的装载,链接(验证、准备、解析)四个阶段都已经完成,也就是上面的第一张图。

  • 装载是指 Java虚拟机查找.class文件并生成字节流,然后根据字节流创建java.lang.Class对象的过程。
  • 链接是指验证创建的类,并将其解析到JVM中使之能够被 JVM 执行。

那到底类加载的时机是什么时候呢?JVM 并没有规范何时具体执行,不同虚拟机的实现会有不同,常见有以下两种情况:

  • 隐式装载:在程序运行过程中,当碰到通过 new 等方式生成对象时,系统会隐式调用 ClassLoader 去装载对应的 class 到内存中;
  • 显示装载:在编写源代码时,主动调用 Class.forName() 等方法也会进行 class 装载操作,这种方式通常称为显示装载。

所以到这里,大的流程框架就搞清楚了:

  • 当JVM碰到new字节码的时候,会先判断类是否已经初始化,如果没有初始化(有可能类还没有加载,如果是隐式装载,此时应该还没有类加载,就会先进行装载、验证、准备、解析四个阶段),然后进行类初始化。
  • 如果已经初始化过了,就直接开始类对象的实例化工作,这时候会调用类对象的<init>方法。

例子说明,然后说说具体的逻辑,结合一段类代码:

public class Run {
    public static void main(String[] args) {
        new Student();
    }
}


public class Person{
    public static int value1 = 100;
    public static final int value2 = 200;

    public int value4 = 400;

    static{
        value1 = 101;
        System.out.println("1");
    }

    {
        value1 = 102;
        System.out.println("3");
    }

    public Person(){
        value1 = 103;
        System.out.println("4");
    }
}

public class Student extends Person{
    public static int value3 = 300;

    public int value5 = 500;

    static{
        value3 = 301;
        System.out.println("2");
    }

    {
        value3 = 302;
        System.out.println("5");
    }

    public Student(){
        value3 = 303;
        System.out.println("6");
    }
}
  • 首先是类装载,链接(验证、准备、解析)。
  • 当执行类准备过程中,会对类中的静态变量分配内存,并设置为初始值也就是“0值”。比如上述代码中的value1,value3,会为他们分配内存,并将其设置为0。但是注意,用final修饰静态常量value2,会在这一步就设置好初始值102。
  • 初始化阶段,会执行类构造器<clinit>方法,其主要工作就是初始化类中静态的(变量,代码块)。但是在当前类的<clinit>方法执行之前,会保证其父类的<clinit>方法已经执行完毕,所以一开始会执行最上面的父类Object的<clinit>方法,这个例子中会先初始化父类Person,再初始化子类Student。
  • 初始化中,静态变量和静态代码块顺序是由语句在源文件中出现的顺序所决定的,也就是谁写在前面就先执行谁。所以这里先执行父类中的value1=100,value1 = 101,然后执行子类中的value3 = 300,value3 = 301。
  • 接着就是创建对象的过程,也就是类的实例化,当对象被类创建时,虚拟机会分配内存来存放对象自己的实例变量和父类继承过来的实例变量,同时会为这些事例变量赋予默认值(0值)。
  • 分配完内存后,会初始化父类的普通成员变量(value4 = 400),和执行父类的普通代码块(value1=102),顺序由代码顺序决定。
  • 执行父类的构造函数(value1 = 103)。
  • 父类实例化完了,就实例化子类,初始化子类的普通成员变量(value5 = 500),执行子类的普通代码块(value3 = 302),顺序由代码顺序决定。
  • 执行子类的构造函数(value3 = 303)。

所以上述例子打印的结果是:

123456

总结一下执行流程就是:

  1. 父类静态变量和静态代码块;
  2. 子类静态变量和静态代码块;
  3. 父类普通成员变量和普通代码块;
  4. 父类的构造函数;
  5. 子类普通成员变量和普通代码块;
  6. 子类的构造函数。

类初始化的触发时机

在同一个类加载器下,一个类型只会被初始化一次,刚才说到new对象是类初始化的一个判断时机,其实一共有六种能够触发类初始化的时机:

  • 虚拟机启动时,初始化包含 main 方法的主类;
  • 遇到 new等指令创建对象实例时,如果目标对象类没有被初始化则进行初始化操作;
  • 当遇到访问静态方法或者静态字段的指令时,如果目标对象类没有被初始化则进行初始化操作;
  • 子类的初始化过程如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化;
  • 使用反射API 进行反射调用时,如果类没有进行过初始化则需要先触发其初始化;
  • 第一次调用java.lang.invoke.MethodHandle 实例时,需要初始化 MethodHandle 指向方法所在的类。

多线程进行类的初始化会出问题吗

不会,<clinit>()方法是阻塞的,在多线程环境下,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>(),其他线程都会被阻塞。

类的实例化触发时机

  • 使用new关键字创建对象
  • 使用Class类的newInstance方法,Constructor类的newInstance方法(反射机制)
  • 使用Clone方法创建对象
  • 使用(反)序列化机制创建对象

<clinit>()方法和<init>()方法区别。

  • <clinit>()方法发生在类初始化阶段,会执行类中的静态类变量的初始化和静态代码块中的逻辑,执行顺序就是语句在源文件中出现的顺序。
  • <init>()方法发生在类实例化阶段,是默认的构造函数,会执行普通成员变量的初始化和普通代码块的逻辑,执行顺序就是语句在源文件中出现的顺序。

在类都没有初始化完毕之前,能直接进行实例化相应的对象吗?

刚才都说了先初始化,再实例化,如果这个问题可以的话那不是打脸了吗?

没错,要打脸了哈哈。

确实是先进行类的初始化,再进行类的实例化,但是如果我们在类的初始化阶段就直接实例化对象呢?比如:

public class Run {
    public static void main(String[] args) {
        new Person2();
    }
}

public class Person2 {
    public static int value1 = 100;
    public static final int value2 = 200;

    public static Person2 p = new Person2();
    public int value4 = 400;

    static{
        value1 = 101;
        System.out.println("1");
    }

    {
        value1 = 102;
        System.out.println("2");
    }

    public Person2(){
        value1 = 103;
        System.out.println("3");
    }
}

嘿嘿,这时候该怎么打印结果呢?

按照上面说过的逻辑,应该是先静态变量和静态代码块,然后普通成员变量和普通代码块,最后是构造函数。

但是因为静态变量又执行了一次new Person2(),所以实例化过程被强行提前了,在初始化过程中就进行了实例化。这段代码的结果就变成了:

23123

所以,实例化不一定要在类初始化结束之后才开始初始化,有可能在初始化过程中就进行了实例化。

类的初始化过程与类的实例化过程的异同?

学了上面的内容,这个问题就很简单了:

  • 类的初始化,是指在类装载,链接之后的一个阶段,会执行<clinit>()方法,初始化静态变量,执行静态代码块等。只会执行一次。
  • 类的实例化,是指在类完全加载到内存中后创建对象的过程,会执行<init>()方法,初始化普通变量,调用普通代码块。可以被调用多次。

一个实例变量在对象初始化的过程中最多可以被赋值几次?

那我们就试试举例出最多的情况,其实也就是每个要经过的地方都对实例变量进行一次赋值:

  • 1、对象被创建时候,分配内存会把实例变量赋予默认值,这是肯定会发生的。
  • 2、实例变量本身初始化的时候,就给他赋值一次,也就是int value1=100。
  • 3、初始化代码块的时候,也赋值一次。
  • 4、构造函数中,在进行赋值一次。

一共四次,看代码:

public class Person3 {
    public int value1 = 100;

    {
        value1 = 102;
        System.out.println("2");
    }

    public Person3(){
        value1 = 103;
        System.out.println("3");
    }
}

代码中修改了UI,屏幕是怎么进行刷新的?

当我们用代码修改了UI,比如使用了setText,修改Textview的值。这时候屏幕不会马上绘制刷新。而是会调用到invalidate方法请求重绘,然后会向VSYNC(垂直同步 帧同步)服务发送请求,等到下一个VSYNC信号触发的时候,就开始上面说过的流程,也就是处理数据,绘制图像,具体所做的工作就是测量—布局—绘制。接着,屏幕就可以拿到缓存区中绘制好的图像并显示到屏幕上了。

所以任何UI的改变,都要遵从上述所说的VSYNC机制,只是这个过程很短。当然为了保证最快时间绘制到屏幕上,而不让其他消息影响到VSYNC的响应速度,就加入了同步屏障。

如果界面保持静止不变,屏幕会刷新吗?图像会被重新绘制吗?

首先,屏幕刷新频率这个是不会变的,也就是每隔16ms左右就会进行一次刷新,而刷新的帧数据就是我们的程序内部在接收到刷新的vsync信号之后,经过计算绘制后的图像数据。

但是,app并不是每一个vsync信号都能接收到的,只有当应用有绘制需求的时候,才会通过scheduledVsync 方法申请VSYNC信号,然后下一个屏幕刷新的信号才能被我们的程序所接收到,也就是Choreographer类的onVsync方法才能被执行,然后就开始测量—布局—绘制等工作了。

所以,如果界面不变化,我们的程序就收不到VSYNC信号,也就无法处理数据进行绘制了

只有当需要改变界面的时候,才会去申请这个屏幕刷新服务,才能接收到VSYNC信号。这种情况下,屏幕还会进行刷新,只不过刷新的都是同样的图像数据。

说说UI(布局)优化

UI优化知识点主要分为三部分:

  • 第一部分,系统为我们做的优化。由于前端中UI展示的特殊性和重要性,Android团队也是在不断想办法提供UI方面的渲染速度等等,所以也是提供了很多方案进行优化,比如:

硬件加速、黄油计划、RenderThread。

  • 第二部分,具体的优化方案。主要包括:

java代码布局、View重用、xml布局优化、异步布局框架Litho、RenderThread与RenderScript、屏幕适配、Flutter、Jetpack Compose

  • 第三部分,工具使用,主要包括:

Choreographer、monitor、Systrace

具体内容可以查看 UI(布局)优化全解析https://mp.weixin.qq.com/s?__biz=MzU0MTYwMTIzMw==&mid=2247485105&idx=1&sn=246b12a78ca4466c28aa970f93a0d31a&scene=21#wechat_redirect

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值