Android应用开发岗 面试汇总-Android基础篇
背景
最近在准备面试,结合之前的工作经验和近期在网上收集的一些面试资料,准备将Android开发岗位的知识点做一个系统的梳理,整理成一个系列:Android应用开发岗 面试汇总。本系列将分为以下几个大模块:
Java基础篇、Java进阶篇、常见设计模式
Android基础篇、Android进阶篇、性能优化
网络相关、数据结构与算法
常用开源库、Kotlin、Jetpack
注1:以上文章将陆续更新,直到我找到满意的工作为止,有跳转链接的表示已发表的文章。
注2:该系列属于个人的总结和网上东拼西凑的结果,每个知识点的内容并不一定完整,有不正确的地方欢迎批评指正。
注3:部分摘抄较多的段落或有注明出处。如有侵权,请联系本人进行删除。
Fragment相关
Activity与Fragment之间生命周期比较
Activity于Fragment的生命周期的不同主要表现在onCreate和onDestroy两个生命周期方法中:
- 在Activity的onCreate方法回调时,在回调期间,Fragment首先会执行
onAttach()-->onCreate()-->onCreateView()-->onActivityCreated() - 在Activity的onDestroy()回调时,在回调期间,Fragment首先会执行onDestroyView()-->onDestroy()-->onDetach()
Fragment 如何实现类似 Activity 栈的压栈和出栈效果?
Fragment 的事物管理器(FragmentManager )内部维持了一个双向链表结构,该结构可以记录我们每次 add 的Fragment 和 replace 的 Fragment,然后当我们点击 back 按钮的时候会自动帮我们实现退栈操作。
Fragment与Activity之间是如何传值的?
- Activity向Fragment传值:将要传的值,放到bundle对象里,在Activity中创建该Fragment的对象fragment,
通过调用 fragment.setArguments()传递到fragment中;在该Fragment中通过调用getArguments()得到bundle对象,就能得到里面的值。 - Fragment向Activity传值:在Activity中拿到Fragment的实例,通过回调的方式,在Activity中实现Fragment的回调。
- 通过在Activity中生成ViewModel,在Fragment使用该ViewModel
Fragment懒加载
为什么要实现懒加载?
在没有添加懒加载之前,只要使用 add+show+hide 的方式控制并显示 Fragment, 那么不管 Fragment 是否嵌套,在初始化后,如果只调用了add+show,同级下的 Fragment 的相关生命周期函数都会被调用。且调用的生命周期函数如下所示:(不管该Fragment是否用户可见,都会调用以下函数,常见在viewpager中)
onAttach -> onCreate -> onCreatedView -> onActivityCreated -> onStart -> onResume
因此,为了避免性能和流量的浪费,需要对当前不可见的Fragment进行懒加载处理,即Fragment真正被用户看到时才开始去加载数据。
add+show+hide 模式下的老方案
场景:通过show+hide方法控制两个fragment的显示,A和B,当从A切换到B时,两个Fragment的可见状态发生了改变,Fragment会回调onHiddenChanged()方法,A中isHidden()方法的值为 true,B中为false。
方案:
- 只要通过 show+hide 方式控制 Fragment 的显隐,那么在第一次初始化后,Fragment 任何的生命周期方法都不会调用,只有 onHiddenChanged 方法会被调用。
- 另,假如我们要在 add+show+hide 模式下控制 Fragment 的懒加载,我们只需要做这两步:
因此
- 1、我们需要在 onResume() 函数中调用 isHidden() 函数,来处理默认显示的 Fragment;
- 2、在 onHiddenChanged 函数中控制其他不可见的Fragment,即:
abstract class LazyFragment:Fragment(){
private var isLoaded = false //控制是否执行懒加载
override fun onResume() {
super.onResume()
judgeLazyInit()
}
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
judgeLazyInit()
}
private fun judgeLazyInit() {
if (!isLoaded && !isHidden()) {
lazyInit()
isLoaded = true
}
}
override fun onDestroyView() {
super.onDestroyView()
isLoaded = false
}
//懒加载方法
abstract fun lazyInit()
}
ViewPager+Fragment 模式下的老方案
ViewPager+Fragment 模式下的老方案
使用传统方式处理 ViewPager 中 Fragment 的懒加载,我们需要控制 setUserVisibleHint(boolean isVisibleToUser) 函数,该函数与之前我们介绍的 onHiddenChanged() 作用非常相似,都是通过传入的参数值来判断当前 Fragment 是否对用户可见,只是 onHiddenChanged() 是在 add+show+hide 模式下使用,而 setUserVisibleHint 是在 ViewPager+Fragment 模式下使用。public void setUserVisibleHint(boolean isVisibleToUser) {}
因此:
abstract class LazyFragment : Fragment() {
//是否执行懒加载
private var isLoaded = false
//当前Fragment是否对用户可见
private var isVisibleToUser = false
/**
* 当使用ViewPager+Fragment形式会调用该方法时,setUserVisibleHint会优先Fragment生命周期函数调用,
* 所以这个时候就,会导致在setUserVisibleHint方法执行时就执行了懒加载,
* 而不是在onResume方法实际调用的时候执行懒加载。所以需要这个变量
*/
private var isCallResume = false
override fun onResume() {
super.onResume()
isCallResume = true
judgeLazyInit()
}
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
isVisibleToUser = !hidden
judgeLazyInit()
}
private fun judgeLazyInit() {
if (!isLoaded && isVisibleToUser && isCallResume) {
lazyInit()
Log.d(TAG, "lazyInit:!!!!!!!")
isLoaded = true
}
}
//在Fragment销毁View的时候,重置状态
override fun onDestroyView() {
super.onDestroyView()
isLoaded = false
isVisibleToUser = false
isCallResume = false
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
this.isVisibleToUser = isVisibleToUser
judgeLazyInit()
}
abstract fun lazyInit()
}
Androidx 下的懒加载
虽然之前的方案就能解决轻松的解决 Fragment 的懒加载,但这套方案有一个最大的弊端,就是不可见的 Fragment 执行了 onResume() 方法。onResume 方法设计的初衷,难道不是当前 Fragment 可以和用户进行交互吗?你他妈既不可见,又不能和用户进行交互,你执行 onResume 方法干嘛?
基于此问题,Google 在 Androidx 在 FragmentTransaction 中增加了 setMaxLifecycle 方法来控制 Fragment 所能调用的最大的生命周期函数。即Fragment被用户可见和可操作时才调用onResume方法:
add+show+hide 模式下的新方案
通过FragmentTransaction 设置setMaxLifecycle的值:setMaxLifecycle(Lifecycle.State.STARTED)
,可以使Fragment生命周期降级到onStart。
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
homefragment = new HomeFragment();
ft.add(R.id.fl_container, homefragment);
ft.setMaxLifecycle(homefragment, Lifecycle.State.STARTED);
ft.commit();
ViewPager+Fragment 模式下的老方案
该场景下,通过调用FragmentStatePagerAdapter中的setPrimaryItem(FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)
注:该方法最终也是调setMaxLifecycle()。
以上两种方式只需要在onResume中增加是否已经加载过数据的判断,来决定是否需要执行初次加载。
参考链接
Service相关
简介:
Service(服务)是一个可以在后台执行长时间运行操作而没有用户界面的应用组件。Service是运行在主线程中的(跟Activity同一个线程),不能进行耗时操作。我们一般都是在Service中创建一个新的线程来处理一些耗时工作,这样就不会阻塞主线程。
启动Service的两种方式
- startService方式启动
如果运行在后台的Service甚至不需要和UI(主)线程间进行交互,这种情况下,一般是调用startService来启动Service。通过 startService启动,Service 会经历 onCreate 到 onStartCommand,然后处于运行状态,stopService的时候调用 onDestroy方法。 如果是调用者自己直接退出而没有调用 stopService 的话,Service 会一直在后台运行。 - bindService方式启动
两个不同进程间通信 或者 某个应用中Service方法的暴露出去(同个进程间),一般是调用bindService来启动Service。通过 bindService启动,Service 会运行 onCreate,然后是调用 onBind, 这个时候调用者和 Service绑定在一起。调用者退出了,Srevice 就会调用 onUnbind->onDestroyed 方法。
所谓绑定在一起就共存亡了,调用者也可以通过调用 unbindService 方法来停止服务。
Service的生命周期
image.png回调方法说明
- onCreate:如果多次执行了Context的startService方法启动Service,Service方法的onCreate方法只会在第一次创建Service的时候调用一次,以后均不会再次调用。我们可以在onCreate方法中完成一些Service初始化相关的操作
- onStartCommand:如果多次执行了Context的startService方法,那么Service的onStartCommand方法也会相应的多次调用。onStartCommand方法很重要,我们在该方法中根据传入的Intent参数进行实际的操作,比如会在此处创建一个子线程用于下载数据或播放音乐等
- onBind:Service中的onBind方法是个抽象方法,所以Service类本身就是一个抽象类,也就是说onBind方法必须要重写,即使用不到。通过startService使用Service时,我们在重写onBind方法时,只需要将其返回值设为null即可。onBind方法主要是用于给bindService方法调用Service时才使用到。
- onDestroy:Service销毁时回调函数
常见面试题
1、什么是 IntentService?有何优点?
- IntentService 是 Service 的子类,比普通的 Service 增加了额外的功能。
先看 Service 本身存在两个问题:
- Service 不会专门启动一条单独的进程,Service 与它所在应用位于同一个进程中;
- Service 也不是专门一条新线程,因此不应该在 Service 中直接处理耗时的任务;
- IntentService 特征
1、会创建独立的 worker 线程来处理所有的 Intent 请求;
2、会创建独立的 worker 线程来处理 onHandleIntent()方法实现的代码,无需处理多线程问题;
3、所有请求处理完成后,IntentService 会自动停止,无需调用 stopSelf()方法停止 Service;
4、为Service 的 onBind()提供默认实现,返回 null;
5、为Service 的 onStartCommand 提供默认实现,将请求 Intent 添加到队列中;
2、怎么保证应用尽可能不被杀死
- 通过保证Service不被销毁的方式(提高Service所在进程的优先级)
- 手机厂商将应用加入白名单
保证Service不被销毁
- 1、在onStartCommand方法中将flag设置为START_STICKY,即return Service.START_STICKY;
- 2、在xml中设置了android:priority
//设置服务的优先级为MAX_VALUE
<service android:name=".MyService"
android:priority="2147483647" >
</service>
- 3、在onStartCommand方法中设置为前台进程
通过设置一个状态栏显示来实现
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Notification notification = new Notification(R.mipmap.ic_launcher, "服务正在运行",System.currentTimeMillis());
Intent notificationIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,notificationIntent,0);
RemoteViews remoteView = new RemoteViews(this.getPackageName(),R.layout.notification);
remoteView.setImageViewResource(R.id.image, R.mipmap.ic_launcher);
remoteView.setTextViewText(R.id.text , "Hello,this message is in a custom expanded view");
notification.contentView = remoteView;
notification.contentIntent = pendingIntent;
startForeground(1, notification);
return Service.START_STICKY;
}
- 4、在onDestroy方法中重启service
@Override
public void onDestroy() {
super.onDestroy();
startService(new Intent(this, MyService.class));
}
- 5、用AlarmManager.setRepeating(…)方法循环发送闹钟广播,接收的时候调用service的onstart方法
Intent intent = new Intent(MainActivity.this,MyAlarmReciver.class);
PendingIntent sender = PendingIntent.getBroadcast( MainActivity.this, 0, intent, 0);
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.add(Calendar.SECOND, 1);
AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE);
//重复闹钟
/**
* @param triggerAtMillis t 闹钟的第一次执行时间,以毫秒为单位
* @param intervalMillis 表示两次闹钟执行的间隔时间,也是以毫秒为单位
* @param operation 绑定了闹钟的执行动作,比如发送一个广播、给出提示等等
*/
am.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), 2 * 1000, sender);
Broadcast Receiver相关
如何注册 BroadcastReceiver
在清单文件中注册广播接收者称为静态注册,在代码中注册称为动态注册。
- 静态注册的广播接收者只要 app 在系统中运行则一直可以接收到广播消息,
- 动态注册的广播接收者当注册的 Activity 或者 Service 销毁了那么就接收不到广播了。
静态注册:在清单文件中进行如下配置
<receiver android:name=".BroadcastReceiver1" >
<intent-filter>
<action android:name="android.intent.action.CALL" > </action>
</intent-filter>
</receiver>
动态注册:在代码中进行如下注册
receiver = new BroadcastReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(CALL_ACTION);
context.registerReceiver(receiver, intentFilter);
Android 引入广播机制的用意?
- 程序间互通消息(例如在自己的应用程序内监听系统来电)
- 效率上(参考 UDP 的广播协议在局域网的方便性)
- 设计模式上(反转控制的一种应用,类似观察者模式)
两种注册各有什么特点
静态注册
- 常驻,当应用程序关闭后如果有信息广播来,程序也会被系统调用,自己运行。
- 无需担忧广播接收器是否被关闭,只要设备是开启状态,广播接收器就是打开着的。
动态注册
- 不常驻,广播会跟随程序的生命周期。
- 在 Android 的广播机制中,动态注册优先级高于静态注册优先级,因此在必要情况下,是需要动态注册广播接收者的。
当用来注册的 Activity 关掉后,广播也就失效了。
BrocastReceiver 的生命周期和注意事项
- BroadCastReceiver 的生命周期很短暂,当接收到广播的时候创建,当onReceive()方法结束后销毁
- 因为BroadCastReceiver的声明周期很短暂,所以不要在广播接收器中去创建子线程做耗时的操作,因为广播接受者被销毁后,这个子进程就会成为空进程,很容易被杀死
- BroadCastReceiver是运行在主线程的,所以不能直接在BroadCastReceiver中去做耗时的操作,否则就会出现ANR异常
参考链接
动画相关
动画有哪几类,各有什么特点?
三类:补间动画、帧动画、属性动画。补间动画和Drawable动画可统称为视图动画。也可以将这三类动画都归为属性动画。
补间动画(View Animation)
在xml中定义,有缩放、平移、渐变、旋转等,可组合使用。动画集合 AnimationSet用来存放上面四种动画的集合
帧动画
也叫Frame动画,可以划分到视图动画的类别。专门用来一个一个的显示Drawable的resources,就像放幻灯片一样
属性动画
通过改变视图的属性,来达到动画的效果的动画。
- 支持对所有View能更新的属性的动画(需要属性的setXxx()和getXxx())。
- 更改的是View实际的属性,所以不会影响其在动画执行后所在位置的正常使用。
参考链接
过渡动画和MotionLayout
- MotionLayout是ConstraintLayout的子类,所以它是一种布局类型,但是它能够为布局属性添加动画效果,是开发者实现动画效果的另一个新的选择。
- 它可以直接通过触摸屏幕来控制动画的运行进度。也就是说MotionLayout会管理你的触摸事件通过跟踪手指的速度,并将其与系统中的视图速度相匹配。从而可以自然地在两者之间通过触摸滑动平稳过渡。并且在动画里面加入了关键帧的概念,使得其自动生成动画在运行时某一阶段会运行到关键帧的状态。
- 它具有ConstraintLayout的所有属性。MotionLayout用来处理两个ConstraintSet之间的切换,并在根据两个ConstraintSet的CustomAttribute参数来自动生成切换动画。
- MotionLayout支持在XML中完全描述一个复杂的动画,而不需要通过Java代码来实现。
Window和WindowManager
类结构图:
image.png
SurfaceView相关
我们知道View是通过刷新来重绘视图,系统通过发出VSSYNC信号来进行屏幕的重绘,刷新的时间间隔是16ms,如果我们可以在16ms以内将绘制工作完成,则没有任何问题,如果我们绘制过程逻辑很复杂,并且我们的界面更新还非常频繁,这时候就会造成界面的卡顿,影响用户体验,为此Android提供了SurfaceView来解决这一问题。如:绘制手写签名
View和SurfaceView的区别:
- 1 . View适用于主动更新的情况,而SurfaceView则适用于被动更新的情况,比如频繁刷新界面。
- 2 . View在主线程中对页面进行刷新,而SurfaceView则开启一个子线程来对页面进行刷新。
- 3 . View在绘图时没有实现双缓冲机制,SurfaceView在底层机制中就实现了双缓冲机制。
作者:冰鉴IT
链接:https://www.jianshu.com/p/b037249e6d31
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
作者:冰鉴IT
链接:https://www.jianshu.com/p/b037249e6d31
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Android中的重要术语解释
- 1.ActivityManagerServices,简称AMS,服务端对象,负责系统中所有Activity的生命周期
- 2.ActivityThread,App的真正入口。当开启App之后,会调用main()开始运行,开启消息循环队列,这就是传说中的UI线程或者叫主线程。与ActivityManagerServices配合,一起完成Activity的管理工作
- 3.ApplicationThread,用来实现ActivityManagerService与ActivityThread之间的交互。在ActivityManagerService需要管理相关Application中的Activity的生命周期时,通过ApplicationThread的代理对象与ActivityThread通讯。
- 4.ApplicationThreadProxy,是ApplicationThread在服务器端的代理,负责和客户端的ApplicationThread通讯。AMS就是通过该代理与ActivityThread进行通信的。
- 5.Instrumentation,每一个应用程序只有一个Instrumentation对象,每个Activity内都有一个对该对象的引用。Instrumentation可以理解为应用进程的管家,ActivityThread要创建或暂停某个Activity时,都需要通过Instrumentation来进行具体的操作。
- 6.ActivityStack,Activity在AMS的栈管理,用来记录已经启动的Activity的先后关系,状态信息等。通过ActivityStack决定是否需要启动新的进程。
- 7.ActivityRecord,ActivityStack的管理对象,每个Activity在AMS对应一个ActivityRecord,来记录Activity的状态以及其他的管理信息。其实就是服务器端的Activity对象的映像。
- 8.TaskRecord,AMS抽象出来的一个“任务”的概念,是记录ActivityRecord的栈,一个“Task”包含若干个ActivityRecord。AMS用TaskRecord确保Activity启动和退出的顺序。如果你清楚Activity的4种launchMode,那么对这个概念应该不陌生。
其他面试题
invalidate、postInvalidate和requestLayout区别
- invalidate在主线程中调用,postInvalidate在子线程中调用,通过ViewRootImpl中的ViewRootHandler进行线程调度,切到主线程,最终调的也是view.invalidate()
- invalidate() -> parent.invalidateChild() -> 层层找到parent,parent.invalidateChildInParent(),直到ViewRootImpl
- ViewRootImpl.invalidateChildInParent() -> invalidateRectOnScreen() -> scheduleTraversals() -> doTraversal() -> performTraversals() -> 根据条件去执行performMeasure() / performLayout / performDraw
- 也就是说任意一个子View调用invalidate,最终都会到ViewRootImpl的performTraversals
- invalidate是在view需要重绘的时候调用,requestLayout方法调用后只会执行当前view的measure和layout,而draw不一定被执行,只有当view的位置发生改变才会执行draw方法
Intent 可以传递哪些数据类型
- Serializable:Java提供的接口,用来对数据进行序列化
- Charsequence:Java提供的接口,实现了这个接口的类有:CharBuffer/String/StringBuffer/StringBuilder这个四个类
- Parcelable:Android提供了一种新的类型:Parcel。本类被用作封装数据的容器,封装后的数据可以通过Intent或IPC传递。 除了基本类型以外,只有实现了Parcelable接口的类才能被放入Parcel中。
- Bundle
- 基础数据类型和String/StringBuffer/StringBuilder
Bitmap和Drawable的区别是什么?
- 可以简单地理解为 Bitmap 储存的是像素信息,Drawable 储存的是 对 Canvas 的一系列操作。而 BitmapDrawable 储存的是「把 Bitmap 渲染到 Canvas 上」这个操作。
- Bitmap:存储图像每个像素数据的容器,是final修饰的类型,不允许被继承
- Drawable:一种图像绘制工具,调用canvas进行绘制,储存的是对Canva的一系列操作
它们之间可以互转吗?理论上是不可以的,但是可以通过自身生成出对方的类型来达到互转的效果。
Webview与Js交互?
Webview调用Js
需要在主线程中调用
- 基本格式:
webView.loadUrl("javascript:methodName(parameterValues)");
- 调用Js无参无返回值函数:
String call = "javascript:readyToGo()";
webView.loadUrl(call);
- 调用Js有参无返回值函数
String call = "javascript:alertMessage(\"" + "content" + "\")";
webView.loadUrl(call);
- 调用Js有参有返回值函数
String call = "javascript:alertMessage(\"" + "content" + "\")";
webView.evaluateJavascript(call, new ValueCallback<String>() {
@Override
public void onReceiveValue(String s) {
Log.d("findCar",s);
}
});
Js通过WebView调用Java代码
从API19开始,Android提供了@JavascriptInterface对象注解的方式来建立起Javascript对象和Android原生对象的绑定,提供给JavScript调用的函数必须带有@JavascriptInterface。
实际上,就是3个步骤:
1.Java被Js调用的方法上加@JavascriptInterface注解;
2.WebView注册JavaScriptInterface;
3.js调用,如window.android.show("JavaScript called~!");
Js调用Android Toast方法
- 1.编写Java原生方法并用使用@JavascriptInterface注解
@JavascriptInterface
public boolean show(String s){
Toast.makeText(getApplication(), s, Toast.LENGTH_SHORT).show();
return true;
}
- 2.注册JavaScriptInterface
webView.addJavascriptInterface(this, "android");
addJavascriptInterface的作用是把this所代表的类映射为JavaScript中的android对象。
- 3.编写JavaScript代码
function toastClick(){
var str=window.android.show("JavaScript called~!");
console.log(str);
}
通过H5打开App的某个页面?
在manifest文件中最开始启动的activity中添加:
<activity android:name=".activitys.MainActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="host"
android:pathPrefix="/pathPrefix"
android:scheme="scheme" />
</intent-filter>
</activity>
//注意host,pathPrefix,scheme都是自己自定义的,只要与h5页面调用的一致即可,如下所示
如果要跳转到指定的页面,在MainActivity的onCreate()中添加:
Intent intent = getIntent();
Uri uri = intent.getData();
if (uri != null) {
String routeId = uri.getQueryParameter("pid");
Intent intent0 = new Intent(MainActivity.this, ZhidingActivity.class);
startActivity(intent0);
}
uri.getQueryParameter("pid");获取h5页面传递的参数,如果没有的话可以忽略
注意一点,微信上对于app的唤醒有拦截,在浏览器中才可以起作用
原文链接:https://blog.csdn.net/m0_37678565/article/details/68491444