Android系统架构分5层。
系统内置的应用程序以及非系统级的应用程序都是属于应用层。
常见的如 拨号器、邮件、日历、相机等
应用框架层为开发人员提供了开发应用程序所需要的API。这一层是用java编写的,所以也叫Java Framework。
各种管理器以及内容提供者和视图系统。
分2部分,C/C++程序库和Android运行时库
硬件抽象层是位于操作系统内核与硬件电路之间的接口层,其目的在于将硬件抽象化。从软硬件测试的角度来看,软硬件的测试工作都可分别基于硬件抽象层来完成,使得软硬件测试工作的并行进行成为可能。
Android 的核心系统服务基于Linux内核,在此基础上添加了部分Android专用的驱动。系统的安全性、内存管理、进程管理、网络协议栈和驱动模型等都依赖于该内核。
Syscall && JNI:Native与Kernel之间有一层系统调用(SysCall)层;Java层与Native(C/C++)层之间有一层纽带JNI。
Binder作为Android系统提供的一种IPC机制。
Client进程向Server进程通信,恰恰是利用进程间可共享的内核内存空间来完成底层通信工作的,Client端与Server端进程往往采用ioctl等方法跟内核空间的驱动进行交互。
此外,内核空间的大小是可以通过参数配置调整的。
Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager以及binder驱动,其中ServiceManager用于管理系统中的各种服务。架构图如下:
注意事项
一次完整的 Binder IPC 通信过程通常是这样:
想进一步了解的可以查看Gityuan大佬系列文章:Binder系列开篇
Socket通信方式也是C/S架构,比Binder简单很多。更多的用于Android framework层与native层之间的通信。
举个例子:App进程的创建就是system_server进程通过Socket发送创建进程请求给Zygote进程,然后Zygote进程fork出来的。
handler用于同进程的线程间通信。
Handler,Message,looper和MessageQueue构成了安卓的消息机制,handler创建后可以通过sendMessage将消息加入消息队列,然后looper不断的将消息从MessageQueue中取出来,回调到Hander的handleMessage方法,从而实现线程的通信。
主线程里不需要手动开启轮询,Activity在构造过程中已经对Looper进行了初始化并且建立了消息循环。在ActivityThread的main方法中创建了一个当前主线程的looper,开启了消息队列。消息队列是一个无限循环。
looper的无限循环为什么不会ANR?
安卓是由事件驱动的,Looper.loop不断的接收处理事件,每一个点击触摸或者Activity每一个生命周期都是在Looper.loop的控制之下的,looper.loop一旦结束,应用程序的生命周期也就结束了。
思考一下发送ANR情况:事件没有得到处理;事件正在处理,但是没有及时完成。 对事件进行处理的就是looper,所以只能说事件的处理如果阻塞会导致ANR,looper的无限循环不会导致AN。
子线程中新建的Handler对象,需要手动初始化Looper。
需要手动调用looper.prepare(),并通过looper.loop()开启消息轮询(循环)。
主线程Looper从消息队列读取消息,当读完所有消息时,主线程阻塞。子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠。因此loop的循环并不会对CPU性能有过多的消耗。
class HandlerThread extends Thread{
@Override
public void run() {
//开始建立消息循环
Looper.prepare();//初始化Looper
handler = new Handler(){//默认绑定本线程的Looper,也可以自己指定
@Override
public void handleMessage(Message msg) {
switch(msg.what){
case 0:
Toast.makeText(MainActivity.this, "子线程收到消息", Toast.LENGTH_SHORT).show();
}
}
};
Looper.loop();//启动消息循环
}
}
不管是插件化还是组件化,都是基于系统的ClassLoader来设计的。只不过Android平台上虚拟机运行的是Dex字节码,一种对class文件优化的产物,传统Class文件是一个Java源码文件会生成一个.class文件,而Android是把所有Class文件进行合并,优化,然后生成一个最终的class.dex,目的是把不同class文件重复的东西只需保留一份,如果我们的Android应用不进行分dex处理,最后一个应用的apk只会有一个dex文件。
Android中的ClassLoader类型也可分为系统ClassLoader和自定义ClassLoader。其中系统ClassLoader包括3种分别是:
有兴趣的可以去SDK里看看源码,示例
SDK\platforms\android-29\android.jar!\dalvik\system\BaseDexClassLoader.class
服务是可以在后台执行长时间运行的操作的应用程序组件。 它不提供用户界面。 一旦启动,即使用户切换到另一个应用程序,服务也可能会继续运行一段时间。 此外,组件可以绑定到服务以与其进行交互,甚至可以执行进程间通信(IPC)。 例如,一项服务可以从后台处理网络事务,播放音乐,执行文件I / O或与内容提供者进行交互。
服务在其托管过程的主线程中运行; 除非另行指定,否则该服务不会创建自己的线程,也不会在单独的进程中运行。
可以在配置文件Androidmanifest.xml中设置Service所在线程。
//前缀“:”意思是将名称附加到程序包的标准进程名称中。 remote可以改成任意自定义名字。
android:process=":remote"
必须显示通知。
当您的应用程序需要执行用户注意到的任务时才应使用前台服务,即使他们没有直接与应用程序进行交互。因此,前台服务必须显示优先级为PRIORITY_LOW或更高的状态栏通知,这有助于确保用户了解您的应用程序在做什么。
示例代码
//关键方法是Service#startForeground(int id, Notification notification)
Intent notificationIntent = new Intent(this, ExampleActivity.class);
PendingIntent pendingIntent =
PendingIntent.getActivity(this, 0, notificationIntent, 0);
Notification notification =
new Notification.Builder(this, CHANNEL_DEFAULT_IMPORTANCE)
.setContentTitle(getText(R.string.notification_title))
.setContentText(getText(R.string.notification_message))
.setSmallIcon(R.drawable.icon)
.setContentIntent(pendingIntent)
.setTicker(getText(R.string.ticker_text))
.build();
//The integer ID that you give to startForeground() must not be 0.
startForeground(1001, notification);
注意:使用的时候,在 onStartCommand 里面调用 startForeground() 方法把 Service 提升为前台进程级别,不要忘记 onDestroy 里面调用 stopForeground () 方法。
一般情况下都是后台服务,后台服务执行用户为直接注意到的操作。
绑定的服务提供了一个客户端-服务器接口,该接口允许组件与该服务进行交互,发送请求,接收结果,甚至通过进程间通信(IPC)跨进程进行交互。 多个组件可以一次绑定到服务,但是当所有组件取消绑定时,该服务将被破坏。
您可以在清单文件中将服务声明为私有服务,并阻止对其他应用程序的访问。
手动调用方法 | 作用 |
---|---|
startService() | 启动服务 |
stopService() | 关闭服务 |
bindService() | 绑定服务 |
unbindService() | 解绑服务 |
内部自动调用的方法 | 作用 |
---|---|
onCreat() | 创建服务 |
onStartCommand() | 开始服务 |
onDestroy() | 销毁服务 |
onBind() | 绑定服务 |
onUnbind() | 解绑服务 |
多次调用startService(),Service#onCreate()只会回调一次,Service#onStartCommand()会回调多次。
正常关闭服务,会回调Service#onDestroy()。
如果是绑定状态的服务,调用stopService()是无效的。此外关闭服务还可以调用Service#stopSelf()。
正常绑定服务,会回调方法Service#onCreate()、Service#onBind()。
正常解绑服务,会先判断是否有调用过Service#onStartCommand(),决定回调Service#onUnbind()后,是否要调用Service#onDestroy()。
常数值:1(0x00000001)。表示Service运行的进程被Android系统强制杀掉之后,Android系统会将该Service依然设置为started状态(即运行状态),但是不再保存onStartCommand方法传入的intent对象,然后Android系统会尝试再次重新创建该Service,并执行onStartCommand回调方法,但是onStartCommand回调方法的Intent参数为null,也就是onStartCommand方法虽然会执行但是获取不到intent信息。如果你的Service可以在任意时刻运行或结束都没什么问题,而且不需要intent信息,那么就可以在onStartCommand方法中返回START_STICKY。
对于在任意时间段内将明确启动和停止运行的事物(例如执行背景音乐播放的服务),此模式有意义。
值为2。(0x00000002)。表示当Service运行的进程被Android系统强制杀掉之后,不会重新创建该Service,当然如果在其被杀掉之后一段时间又调用了startService,那么该Service又将被实例化。
如果我们某个Service执行的工作被中断几次无关紧要或者对Android内存紧张的情况下需要被杀掉且不会立即重新创建这种行为也可接受,那么我们便可将onStartCommand的返回值设置为START_NOT_STICKY。比较好的例子就是,轮询对于来自服务器的数据,用一个定时器,指定每过5分钟,启动Service去获取服务器数据(数据不能太多,Service超时时间是20s),假设Service在从服务器获取最新数据的过程中被Android系统强制杀掉,Service不会再重新创建,这都不是事,再过5分钟定时器又启动Service了。
值为3。(0x00000003)表示Service运行的进程被Android系统强制杀掉之后,与返回START_STICKY的情况类似,Android系统会将再次重新创建该Service,并执行onStartCommand回调方法,但是不同的是,Android系统会再次将Service在被杀掉之前最后一次传入onStartCommand方法中的Intent再次保留下来并再次传入到重新创建后的Service的onStartCommand方法中,这样我们就能读取到intent参数。
适用于Service需要依赖具体的Intent才能运行(需要从Intent中读取相关数据信息等),并且在强制销毁后有必要重新创建运行的情况。
值为0。(0x00000000)
从onStartCommand(Intent,int,int)返回的常量:START_STICKY的兼容版本,它不保证onStartCommand(Intent,int,int)被杀死后将再次被调用。
intentService是Service的子类,继承service,拥有service的全部生命周期,包含了service的全部特性。
//部分IntentService源码,基于sdk28
private volatile Looper mServiceLooper;
private volatile ServiceHandler mServiceHandler;
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}
@Override
public void onCreate() {
super.onCreate();
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}
public IntentService(String name) {
super();
mName = name;
}
@Override
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
onStart(intent, startId);
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}
@Override
public void onStart(@Nullable Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}
分析:
最开始只有一块运行着原始 Android 系统的板砖。
Surface Flinger 的出现是为了更加方便地完成 UI 渲染。
Window 的出现是为了管理 UI 内容的排版。
Window 觉得负担太重将责任下发到 View 身上。
View 通过组合模式,在递归的帮助下蹭蹭蹭地完成排版工作。
Activity 的出现是为了满足多窗口管理和傻瓜式视图管理的需要。
Android是基于linux系统的,手机开机之后,linux内核进行加载。加载完成之后会启动init进程。
init进程会启动ServiceManager,孵化一些守护进程,并解析init.rc孵化Zygote进程。
所有的App进程都是由Zygote进程fork生成的,包括SystemServer进程。Zygote初始化后,会注册一个等待接受消息的socket,OS层会采用socket进行IPC通信。
每个应用程序都是运行在各自的Dalvik虚拟机中,应用程序每次运行都要重新初始化和启动虚拟机,这个过程会耗费很长时间。Zygote会把已经运行的虚拟机的代码和内存信息共享,起到一个预加载资源和类的作用,从而缩短启动时间。
抛开热启动的情况,比如应用在后台时点击app图标。这时候直接走走Activity生命周期就行。下面我们讨论冷启动的情况。
从点击屏幕上的app图标,到进入响应app,背后经历了Activity和AMS(ActivityManagerService)的反反复复的通信过程。在AndroidManifest.xml文件中定义默认启动的activity,设置activity的action和category属性标签。Launcher为每个app的icon图标提供了启动这个app时所需要的Intent信息。这些信息在app安装时由PackageManagerService从app的AndroidManifest.xml文件中读取。
下图适用于api26之前,因为api26(8.0)后android framework代码发生了变化,部分类被废弃和移除了。
比如在26sdk里ActivityManagerProxy被移除了,方式改变了
sdk版本为28,截取部分Instrumentation类的exeStartActivity方法,ActivityManager.getService()实现使用的aidl。
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, String target,
Intent intent, int requestCode, Bundle options) {
//省略...
int result = ActivityManager.getService()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target, requestCode, 0, null, options);
checkStartActivityResult(result, intent);
//省略...
}
而在sdk版本25时候,是使用ActivityManagerNative.getDefault()获取ActivityManagerProxy执行其startActivity方法。26之后简化了不少代码,明显看出使用了aidl,而之前使用的Binder,讲实话AIDL是基于Binder机制实现的,所以其实差不多。
由于优先级和资源回收的原因,可能同一个操作在不同场景的生命周期不一样。下文如果未特别说明,默认资源没有被回收。
onCreate:create表示创建,这是Activity生命周期的第一个方法,也是我们在android开发中接触的最多的生命周期方法。此时Activity还在后台,不可见。所以动画不应该在这里初始化。
onStart:start表示启动,这是Activity生命周期的第二个方法。此时Activity已经可见了,但是还没出现在前台,我们还看不到,无法与Activity交互。
onResume:在前台,和onStart同样可见,不同的是onStart在后台,onResume在前台。
-这个阶段可以打开独占设备。(可与用户交互,我们称为独占设备)
onPause:正在停止。此时Activity在前台并可见,我们可以进行一些轻量级的存储数据和去初始化的工作。不能做耗时操作,因为在跳转Activity时只有当一个Activity执行完了onPause方法后另一个Activity才会启动,而且android中指定如果onPause在500ms即0.5秒内没有执行完毕的话就会强制关闭Activity。
onStop:stop表示停止,此时Activity已经不可见了,但是Activity对象还在内存中,没有被销毁。这个阶段的主要工作也是做一些资源的回收工作。
onDestroy:destroy表示毁灭,可以理解为之后Activity被销毁,我们可以将还没释放的资源释放,以及进行一些回收工作。
onRestart:restart表示重新开始,Activity在这时可见。这个方法一般不做操作。
对于在其程序包中将launchMode设置为“singleTop”的活动,或者在客户端调用startActivity(Intent)时使用Intent#FLAG_ACTIVITY_SINGLE_TOP标志的活动,将调用此方法。
不论在哪种情况,当活动在活动堆栈的顶部重新启动对于正在启动的活动的新实例,将使用onNewIntent()调用现有实例,其目的是用于重新启动它。
举例说明
总结:只要不是standard模式,其他3个模式下都可能调用onNewIntent方法。无非是启动自己或者从其他界面启动他。
Activity的异常情况下(例如转动屏幕或者被系统回收的情况下),会调用到onSaveInstanceState和onRestoreInstanceState。其他情况不会触发这个过程。但是按Home键或者启动新Activity或者锁屏仍然会单独触发onSaveInstanceState的调用。
onSaveInstanceState()的调用遵循一个重要原则,即当系统存在“未经你许可”时销毁了我们的activity的可能时,则onSaveInstanceState()会被系统调用,这是系统的责任,因为它必须要提供一个机会让你保存你的数据。以下情况都会调用,可能是单独触发保存,也可能出现重建,触发onRestoreInstanceState方法。
旧的Activity要被销毁时,由于是异常情况下的,所以除了正常调用onPause, onStop, onDestroy方法外,还会在调用onStop方法前,调用onSaveInstanceState方法。
新的Activity重建时,我们就可以通过onRestoreInstanceState方法取出之前保存的数据并恢复,onRestoreInstanceState的调用时机在onCreate之后。onRestoreInstanceState()在onStart()和onPostCreate(Bundle)之间调用
google在android3.2中添加了screensize改变的通知,在转屏的时候,不仅是orientation发生了改变,screensize同样也发生了改变。都2020年了,目标版本大家都写的挺高的,姑且还行记一下吧。
简单点说就是销毁了再新建。此外onSaveInstanceState调用时机以实践结果为例,并不代表onSaveInstanceState调用不能在onPause之前。
onPause→onSaveInstanceState(Bundle outState)→onStop→onDestroy→onCreate→onStart→onResume
onSaveInstanceState(Bundle outState)→onPause→onStop→onDestroy→onCreate→onStart→onResume
最后提一哈,网上说的什么横屏执行1次,竖屏执行2次,还有设置参数keyboardHidden。其实都是误导,我实践了发现只有screenSize配合orientation才有用。
//应用活动没有在锁屏时被回收,所以调用onRestart
//锁屏
onPause→onSaveInstanceState→onStop
//解锁
onRestart→onStart→onResume
A:onPause ->onSaveInstanceState-> B:onCreate -> B:onStart -> B:onResume -> A:onStop
onPause→onSaveInstanceState→onStop
无论任务栈中是否已经有这个Activity的实例,系统都会创建一个新的Activity实例。
默认模式,通常会在启动活动时创建一个新的活动实例,尽管这种行为可能会因引入其他选项(例如Intent.FLAG_ACTIVITY_NEW_TASK)而改变。
当一个singleTop模式的Activity已经位于任务栈的栈顶,再去启动它时,不会再创建新的实例,如果不位于栈顶,就会创建新的实例,只有重新启动时候才会调用onNewIntent。
如果在启动活动时,前台中已经存在与用户交互的同一活动类的实例,则请重新使用该实例。 此现有实例将通过新的Intent收到对Activity.onNewIntent()的调用。如果没有,则会正常新建一个并入栈。
如果Activity已经位于栈顶,系统不会创建新的Activity实例,和singleTop模式一样。但Activity已经存在但不位于栈顶时,系统就会把该Activity移到栈顶,并把它上面的activity出栈。这个模式活动如果栈里有实例,则再次调用一定跑onNewIntent。
如果在启动活动时已经有一个以该活动开始的任务正在运行,那么将当前任务置于最前面,而不是启动新实例。现有实例将收到一个Activity.onNewIntent()的调用,该调用具有正在启动的新Intent,并且设置了Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT标志。 这是singleTop模式的超集,在该模式下,如果已经在堆栈的顶部启动了活动的实例,它将接收那里描述的Intent(不设置FLAG_ACTIVITY_BROUGHT_TO_FRONT标志)。
singleInstance模式也是单例的,但和singleTask不同,singleTask只是任务栈内单例,而 singleInstance修饰的活动整个系统只有一个。启动一个SingleInstance模式的活动时,系统会创建一个新的任务栈,而且这个任务栈里只有一个实例。
还有个细节是,SingleInstance的活动实例化的时候,如果存在,直接调用onNewIntent。
关于这种模式需要注意,因为在SingleInstance活动里打开其他活动时,都是放在活动应该放的栈里,所以会造成返回并返回不到这个SingleInstance活动的情况,以及出现当前交互界面是某个SingleInstance活动,摁home键回到桌面,如果不是在最近活动里选择,直接点击app应用,会直接回到应用最后启动的活动界面,不会回到SingleInstance活动。
这样设计也有好处,实现了多个应用共享一个应用,使用场景如闹钟、相机等。
在新任务中启动 Activity。如果您现在启动的 Activity 已经有任务在运行,则系统会将该任务转到前台并恢复其最后的状态,而 Activity 将在 onNewIntent() 中收到新的 intent。
注意事项
//api29源码ContextImpl.java中的startActivity()有如下判断,印证了建议显示设置标记的原因。
if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == 0
&& (targetSdkVersion < Build.VERSION_CODES.N
|| targetSdkVersion >= Build.VERSION_CODES.P)
&& (options == null
|| ActivityOptions.fromBundle(options).getLaunchTaskId() == -1)) {
throw new AndroidRuntimeException(
"Calling startActivity() from outside of an Activity "
+ " context requires the FLAG_ACTIVITY_NEW_TASK flag."
+ " Is this really what you want?");
}
Intent.FLAG_ACTIVITY_NEW_TASK的初衷是在Activity目标taskAffinity的Task中启动
如果不是在Activity中启动的,那就可以看做不是用户主动的行为,也就说这个界面可能出现在任何APP之上,如果不用Intent.FLAG_ACTIVITY_NEW_TASK将其限制在自己的Task中,那用户可能会认为该Activity是当前可见APP的页面,这是不合理的。
我举个例子。比如打开QQ音乐后回到桌面,此时QQ音乐在后台,我再打开新浪微博,这时候QQ音乐发来了一个推送消息告诉我我的会员需要续费了,我点击推送消息进入QQ音乐开通VIP的界面,这时候摁返回键,并不会回到新浪微博,而是QQ音乐的主页。
Intent.FLAG_ACTIVITY_SINGLE_TOP多用来做辅助作用,跟launchmode中的singleTop作用一样。如果要启动的 Activity是当前Activity(即位于返回堆栈顶部的Activity),则现有实例会收到对 onNewIntent() 的调用,而不会创建 Activity 的新实例。(onPause->onNewIntent->onResume)
这种情况也比较复杂,通常是结合其他标签一起使用。
目标是当前Task栈,栈里有实例时,则不会启动该 Activity 的新实例,而是会销毁位于它之上的所有其他 Activity,并通过 onNewIntent() 将此 intent传送给它的已恢复实例(现在位于堆栈顶部)。(调用onNewIntent之后会调用onCreate,可以视为是新的)
同单独使用FLAG_ACTIVITY_CLEAR_TOP差不多,区别在于,不会"NEW",只调用onNewIntent
因为FLAG_ACTIVITY_NEW_TASK 的存在,所以配置文件里的android:taskAffinity属性能起到作用,正常情况下会新开辟一个任务栈。如果当前任务栈找不到,会去目标Task中去找。
同3一样,只不过找到之后清空顶部,直接调用onNewIntent,不会新建。
如果这么设置,并且此活动是在新任务中启动或将现有任务置于顶部,则它将作为任务的前门启动。
仅在需要时候,将将任务重置为初始状态。我们知道任务栈里有很多活动Activity,使用FLAG_ACTIVITY_RESET_TASK_IF_NEEDED常见例子:从桌面打开app。之后摁home键回到桌面,再次点击app图标,会回到app之前停留的页面。没错,这种情况没有重置任务。
当我们将一个后台的task重新回到前台时,系统会在特定情况下为这个动作附带一个FLAG_ACTIVITY_RESET_TASK_IF_NEEDED标记,意味着必要时重置task,这时FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET就会生效。经过测试发现,对于一个处于后台的应用,如果在launcher中点击应用,这个动作中含有FLAG_ACTIVITY_RESET_TASK_IF_NEEDED标记,长按Home键,然后点击最近记录,这个动作不含FLAG_ACTIVITY_RESET_TASK_IF_NEEDED标记,所以前者会清除(仅清除使用Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET启动的所有活动以及这些活动之上的所有活动),后者不会。
该标志通常不是由应用程序代码设置的,而是由系统为您设置的,比如上文singleTask就有提到。
这个用到的可能性比较大, 会是活动在栈中的位置发生改变。比如说原来栈中情况是A,B,C,D
,在D中启动B(加入该flag),栈中的情况会是A,C,D,B
(并且调用onNewIntent)
“亲和性”表示 Activity 倾向于属于哪个任务。默认情况下,同一应用中的所有 Activity 彼此具有亲和性。
亲和性可在两种情况下发挥作用:
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime)
作用:当某些更改使该视图的布局无效时调用此方法。 这将安排视图树的布局遍历。
时机:当视图层次结构当前处于布局阶段({@link#isInLayout()}中时,不应调用此方法。如果正在进行布局,则可以在当前布局阶段结束时接受该请求(然后布局将再次运行) )或绘制当前帧并进行下一个布局之后。
注意事项:只是对View树重新布局layout过程包括measure()和layout()。如果view的l,t,r,b没有必变,那就不会触发onDraw;但是如果这次刷新是在动画里,mDirty非空,就会导致onDraw。
一说到绘制流程,就会想到onMeasure、onLayout、onDraw这三个方法,但是有没想过为什么我们开启一个App或是点开一个Activity就会触发这一系列流程呢?有必要先理一理App启动流程和Activity的启动流程。上文有详细介绍,这里简单回顾下:
ActivityThread的main方法。里面做了一系列操作,比如开启主线程的消息轮询(Looper初始化);再比如通过ActivityManager的getService获取ActivityManagerService的代理,达到跨进程通信目的。
源码不深入研究了,大概知道顺序就行Activity-》Window->View
知识点
//视图的实际测量工作执行在这里,被measure方法拉起的
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
MeasureSpec封装了从父级传递到子级的布局要求。系统把view的LayoutParams 根据 父容器施加的规则(父容器的SpecMode) 转换成 view的MeasureSpec,然后使用这个MeasureSpec确定view的测量宽高。View的MeasureSpec是由LayoutParams和父容器的MeasureSpec共同决定。顶级view,即DecorView,是由窗口尺寸和自身LayoutParams决定。
MeasureSpec即view的测量规格:高2位的SpecMode,低30位的SpecSize。
UNPECIFIED父容器对view不限制,要多大给多大,一般系统内部使用。
EXACTLY,父容器检测出view所需大小,view最终大小就是SpecSize的值。对应 LayoutParams中的matchParent、具体数值 两种模式。
AT_MOST,父容器制定了可用大小即SpecSize,view的大小不能大于这个值,具体要看view的具体实现。对应LayoutParams中的wrap_content。
View类中onMeasure是设置了固定宽高的,看方法
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
UNSPECIFIED,一般是系统使用,暂时先不关心他。这里view大小直接取size,就是getSuggestedMinimumWidth()/getSuggestedMinimumHeight(),意思是建议的最小宽高。
在日常定制View时,确实很少会专门针对这个模式去做特殊处理,大多数情况下,都会把它当成MeasureSpec.AT_MOST一样看待,就比如最最常用的TextView,它在测量时也是不会区分UNSPECIFIED和AT_MOST的。
不过,虽说这个模式比较少直接接触到,但很多场景下,我们已经在不知不觉中用上了,比如RecyclerView的Item,如果Item的宽/高是wrap_content且列表可滚动的话,那么Item的宽/高的测量模式就会是UNSPECIFIED。还有就是NestedScrollView和ScrollView,因为它们都是扩展自FrameLayout,所以它们的子View会测量两次,第一次测量时,子View的heightMeasureSpec的模式是写死为UNSPECIFIED的。
我们在自定义ViewGroup过程中,如果允许子View的尺寸比ViewGroup大的话,在测量子View时就可以把Mode指定为UNSPECIFIED。
AT_MOST、EXACTLY,直接取specSize。specSize确定综合父亲控件和当前控件进行分析。
childLayoutParams(纵)/parentSpecMode | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
dp/px | EXACTLY childSize | EXACTLY childSize | EXACTLY childSize |
match_parent | EXACTLYparentSize | AT_MOST parentSize | UNSPECIFIED 0 |
wrap_content | AT_MOST parentSize | AT_MOST parentSize | UNSPECIFIED 0 |
值得注意的是,有时候你会发现你自定义View的wrap_content竟然不起作用,还有padding也不起作用,下面我们来分析一波:
protected void onLayout(boolean changed, int l, int t, int r, int b)
onLayout 则是进行摆放,这一过程比较简单,因为我们从 onMeasure 中已经得到各个子View 的宽高。父View 只要按照自己的逻辑负责给定各个子View 的 左上坐标 和 右下坐标 即可。
protected void onDraw(Canvas canvas)
在 canvas 绘制自身需要绘制的内容。
举个例子:比如我旋转坐标轴30度画一些东西,这时候原来的x、y相对旋转了30度,为了避免之后的绘制也在这个新坐标轴上画,使用restore回到sava的那个坐标轴。
drawText (CharSequence text, int start, int end, float x, float y, Paint paint)
在指定的Paint中绘制由start / end指定的指定范围的文本,其原点为(x,y)。 根据对齐设置解释原点。画笔的对齐设置,默认是从左往右,还可以设置居中:
textPaint.setTextAlign(Paint.Align.CENTER);
//原点为(x,y)
drawText (String text, float x, float y, Paint paint)
drawTextOnPath (String text, Path path, float hOffset, float vOffset, Paint paint)
沿着指定的路径使用指定的画笔绘制以(x,y)为原点的文本。 绘画的对齐设置确定路径从何处开始文本。
//要求api达到23
drawTextRun (char[] text, int index, int count,
int contextIndex, int contextCount, float x,
float y, boolean isRtl, Paint paint)
在一个方向上绘制一行文本,并带有可选的上下文,以进行复杂的文本成形。
文本行中包含从头到尾的字符。另外,范围contextStart到contextEnd用作上下文,用于复杂的文本整形,例如阿拉伯文本可能根据其旁边的文本而具有不同的形状。
超出contextStart…contextEnd范围的所有文本都将被忽略。开始和结束之间的文本将被布局和绘制。上下文范围对于上下文整形很有用,例如紧缩,阿拉伯语语境形式。
文字方向由isRtl明确指定。因此,此方法仅适用于单方向行驶。文本的对齐方式由Paint的TextAlign值确定。此外,0 <= contextStart <= start <=结束<= contextEnd <= text.length必须在输入时保持。
Path经常用于自定义View,界面绘制,配合canvas.drawPath使用。
moveTo用来移动画笔。移动过程不会有痕迹。
用来画直线,默认是左上角(0,0)坐标。
用来画二次贝塞尔曲线,起始默认点为(0,0)。
public void quadTo (float x1, float y1, float x2, float y2)
解释:从最后一个点开始添加一个二次贝塞尔曲线,逼近控制点(x1,y1),并在(x2,y2)处结束。 如果没有为此轮廓调用moveTo(),则第一个点将自动设置为(0,0)。
用来画三次贝塞尔曲线,起始默认点为(0,0)。
public void cubicTo (float x1, float y1, float x2, float y2, float x3, float y3)
解释:从最后一点添加一个三次方贝塞尔曲线,逼近控制点(x1,y1)和(x2,y2),并在(x3,y3)处结束。 如果没有为此轮廓调用moveTo(),则第一个点将自动设置为(0,0)。
注意事项:前2个点是控制点。
以A、B 2个点为例,B是结束点。先算出控制点F1和F2:x坐标为A、B重点,y坐标分别为A.y和B.y
用来画圆弧,虽然贝塞尔曲线也能绘制,但是这个常用于截取(椭)圆的一部分。
public void arcTo(RectF oval, float startAngle, float sweepAngle)
解释:将指定的弧形作为新轮廓附加到路径。
如果路径的起点与路径的当前最后一个点不同,则将添加自动lineTo()以将当前轮廓连接到弧的起点。 但是,如果路径为空,则使用圆弧的第一点调用moveTo()。
例子:截取一个正方形的内切圆的右下角一段圆弧path
mRectF = new RectF(10, 10, 300, 300);
//3点钟方向开始,顺时针
mPath.arcTo(mRectF, 0, 90);
canvas.drawPath(mPath, mPaint);
public void arcTo(RectF oval, float startAngle, float sweepAngle,boolean forceMoveTo)
比上面的方法多了一个forceMoveTo变量。
分析一波:这个变量指的是,是否要和上一次操作的点连起来,设置为true,则始终以圆弧开始新轮廓,不管你path之前在哪个点,都直接从区域oval内开始绘制圆弧。如果为false,则会连起来,比如开始画圆时,坐标在(100,100),那么开始画圆弧之前会lineTo圆弧的起点。
api21出现的方法
public void arcTo (float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo)
向路径添加封闭的圆角矩形轮廓。
//此方法 rect和dir不能为空,必须有一个矩形,设置他的上圆角的x、y半径,最后设置缠绕矩形轮廓的方向
public void addRoundRect (RectF rect, float rx, float ry, Path.Direction dir)
api21出现的方法
//这个好理解,通过left、top等参数,省去了RectF的定义
public void addRoundRect (float left,
float top,
float right,
float bottom,
float rx,
float ry,
Path.Direction dir)
Direction 有顺时针和逆时针 CW、CCW
关闭当前轮廓。 如果当前点不等于轮廓的第一个点,则会自动添加线段。
清除路径中的所有直线和曲线,使其为空。 这不会更改填充类型设置。
//也可以通过Paint#setFlags设置
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
Paint.ANTI_ALIAS_FLAG :抗锯齿标志
Paint.FILTER_BITMAP_FLAG : 使位图过滤的位掩码标志
Paint.DITHER_FLAG : 使位图进行有利的抖动的位掩码标志
Paint.UNDERLINE_TEXT_FLAG : 下划线
Paint.STRIKE_THRU_TEXT_FLAG : 中划线
Paint.FAKE_BOLD_TEXT_FLAG : 加粗
Paint.LINEAR_TEXT_FLAG : 使文本平滑线性扩展的油漆标志
Paint.SUBPIXEL_TEXT_FLAG : 使文本的亚像素定位的绘图标志
Paint.EMBEDDED_BITMAP_TEXT_FLAG : 绘制文本时允许使用位图字体的绘图标志
对齐设置
//居中
setTextAlign(Paint.Align.CENTER)
为Paint添加渐变器。
以线性渐变为例
//为Paint设置渐变器
hader mShader = new LinearGradient(0,0,40,60,
new int[]{
Color.RED,Color.GREEN,Color.BLUE,Color.YELLOW},
null,Shader.TileMode.REPEAT);
//为Paint设置渐变器
paint.setShader(mShader);
关于LinearGradient
/**
* x0 起点x坐标
* y0 起点y坐标
* x1
* y1
* colors 渐变颜色集合
* positions 颜色数组中每种相应颜色的相对位置[0..1]。如果为null,则颜色会沿着渐变线均匀分布。 该值可以为空。
* tile 着色器拼贴模式,此值不得为null。{Shader.TileMode.CLAMP、Shader.TileMode.REPEAT}
**/
public LinearGradient (float x0,
float y0,
float x1,
float y1,
int[] colors,
float[] positions,
Shader.TileMode tile)
设置Paint画出虚线
//设置画笔画出虚线 PathEffect使用
//两个值分别为循环的实线长度、空白长度
float[] f = {dp2pxF(5f), dp2pxF(2f)};
PathEffect pathEffect = new DashPathEffect(f, 0);
paint.setPathEffect(pathEffect);
/**
* dp转pxF
* MyApplication为工程自定义application类
*/
public static float dp2pxF(float dpValue) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, MyApplication.getInstance().getResources().getDisplayMetrics());
}
可以设置自定义字体
Typeface createFromAsset(AssetManager mgr, String path) //通过从Asset中获取外部字体来显示字体样式
Typeface createFromFile(String path)//直接从路径创建
Typeface createFromFile(File path)//从外部路径来创建字体样式
示例
//...省略其他代码
Typeface typeface=Typeface.createFromAsset(mContext.getAssets(), "fonts/hwxk.ttf");
mPaint.setTypeface(typeface);
canvas.drawText(text, 10,100, mPaint);
//...省略其他代码
String str = "Hello";
//1. 粗略计算文字宽度
Log.d(TAG, "measureText=" + paint.measureText(str));
//2. 计算文字所在矩形,可以得到宽高
Rect rect = new Rect();
paint.getTextBounds(str, 0, str.length(), rect);
int w = rect.width();
int h = rect.height();
Log.d(TAG, "w=" +w+" h="+h);
//3. 精确计算文字宽度
int textWidth = getTextWidth(paint, str);
Log.d(TAG, "textWidth=" + textWidth);
public static int getTextWidth(Paint paint, String str) {
int iRet = 0;
if (str != null && str.length() > 0) {
int len = str.length();
float[] widths = new float[len];
paint.getTextWidths(str, widths);
for (int j = 0; j < len; j++) {
iRet += (int) Math.ceil(widths[j]);
}
}
return iRet;
}
参考我之前写的一篇文章理解事件分发中的3个方法
①如果onTouch()方法返回值是true(事件被消费)时,则onTouchEvent()方法将不会被执行;
②只有当onTouch()方法返回值是false(事件未被消费,向下传递)时,onTouchEvent方法才被执行。
③平时我们使用的OnClickListener,其优先级最低,即处于事件传递的尾端。(实现都基于onTouchEvent)
④给View设置监听OnTouchListener,重写onTouch()方法。其优先级比onTouchEvent()高。如果不返回false,那么设置的点击监听等也就意味着失效了。
注意事项
举例
//app可以改成你想的任意名字
xmlns:app="http://schemas.android.com/apk/res-auto"
(onInterceptTouchEvent),如果父容器需要则拦截,如果不需要则不拦截,称为外部拦截法。
父容器不拦截任何事件,将所有事件传递给子元素,如果子元素需要则消耗掉,如果不需要则通过requestDisallowInterceptTouchEvent方法(请求父类不要拦截,返回值为true时不拦截,返回值为false时为拦截)交给父容器处理,称为内部拦截法.
SurfaceView中采用了双缓冲机制,保证了UI界面的流畅性,同时SurfaceView不在主线程中绘制,而是另开辟一个线程去绘制,所以它不妨碍UI线程;
SurfaceView继承于View,他和View主要有以下三点区别:
View:显示视图,内置画布,提供图形绘制函数、触屏事件、按键事件函数等;必须在UI主线程内更新画面,速度较慢。
SurfaceView:基于view视图进行拓展的视图类,更适合2D游戏的开发;是view的子类,类似使用双缓机制,在新的线程中更新画面所以刷新界面速度比view快,Camera预览界面使用SurfaceView。
GLSurfaceView:基于SurfaceView视图再次进行拓展的视图类,专用于3D游戏开发的视图;是SurfaceView的子类,openGL专用。
参考:5.0源码中的SurfaceView、TextureView的介绍
精简版
名词解释
流程
api (某第三方资源)
{
exclude group: 'com.android.support'
}
api (某第三方资源)
{
//假如没有使用到库中的glide,加上这句移除对Glide的依赖以减小包体积
exclude module: 'glide'
}
解决这种冲突,涉及到Andoird资源的编译和打包原理。市面上基本都是通过修改aapt的产物或者定制aapt来解决。
安装过程其实非常复杂,下面先记录一下简化后的流程
Proguard是一个集文件压缩,优化,混淆和校验等功能的工具
作用:混淆将主项目及依赖库中未被使用的类,类成员,方法,属性移除,有助于规避64K方法数的瓶颈,会删除无用的资源,有效的减小apk包的大小,同时将类及其成员,方法重命名为无意义的简短名称,增加逆向工程的难度。
Android Studio 3.4 或 Android Gradle 插件 3.4.0 及更高版本时,R8是默认编译器,用于将项目的 Java 字节码转换为在 Android 平台上运行的 DEX 格式。3.4.0之前老版本插件使用 ProGuard 。
R8 在每次运行时都会创建一个mapping.txt文件,其中列出了经过混淆处理的类、方法和字段名称与原始名称的映射关系。(/build/outputs/mapping// 目录)
android {
buildTypes {
release {
// Enables code shrinking, obfuscation, and optimization for only
// your project's release build type.
minifyEnabled true
// Enables resource shrinking, which is performed by the
// Android Gradle plugin.
shrinkResources true
// Includes the default ProGuard rules files that are packaged with
// the Android Gradle plugin. To learn more, go to the section about
// R8 configuration files.
proguardFiles getDefaultProguardFile(
'proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}
...
}
ProGuard官方使用手册:ProGuard使用
可阅读:代码混淆做了啥
总结
因为是多方决定的最终混淆文件,所以也不列举例子了,具体怎么写可以查看ProGuard官方文档。
规则文件来源 | 描述 |
---|---|
Android Gradle 插件 | 在编译时,由 Android Gradle 插件生成 |
AAPT2 | 在编译时,AAPT2 根据对应用清单中的类、布局及其他应用资源的引用来生成保留规则 |
Module | 创建新 Module 时,由 IDE 创建,或者另外按需创建 |
将 minifyEnabled 属性设为 true,ProGuard 或 R8 会将来自上面列出的所有可用来源的规则组合在一起
。为了看到完整的规则文件,可以在proguard-rules.pro 中添加以下配置,输出编译项目时应用的所有规则的完整报告:
-printconfiguration build/intermediates/proguard-files/full-config.txt
-injars 'E:\myjar.jar' #需要混淆的jar文件和路径
-outjars 'E:\myjar_out.jar' #混淆后的jar文件名字和路径
-libraryjars 'D:\Sdk\platforms\android-29\android.jar' #jar依赖的包
https://developer.android.com/training/volley?hl=zh-cn
背景:自从Android4.4开始,google已经开始将源码中的HttpURLConnection替换为OkHttp,而在Android6.0之后的SDK中google更是移除了对于HttpClient的支持,而市面上流行的Retrofit同样是使用OkHttp进行再次封装而来的。
老牌图片加载库。2011年项目开始维护,2015停止维护。对于那个时候的安卓生态圈来说,ImageLoader真的挺牛的。
Volley作为网络请求库,还能用于图片加载。Google官方文档上有介绍。
参考:使用Volley加载网络图片
GitHub: https://github.com/square/picasso
和Square的网络库一起能发挥最大作用,因为Picasso可以选择将网络请求的缓存部分交给了okhttp实现。毕竟是Square公司出品,JakeWharton大神带头研发的。Picasso不支持gif。优点是体积小,如果没有特别的需要,可以选择他。(Square全家桶干活)
GitHub: https://github.com/bumptech/glide
Picasso能干的,Glide都能干,Glide能干的,Picasso不一定能干。
v3版本的Glide中默认使用HttpUrlConnection来加载网络图片。如果你应用中添加了OkHttp,Glide会自动合并使用Okhttp来加载网络图片。如果项目里添加2个网络库,比如OkHttp和Volley,那Glide就处于一个不稳定状态,用哪个网络库看心情。所以自己实现一个AppGlideModule进行配置就显得很重要了。
虽然v4应该改了不少默认配置,但是自定义AppGlideModule还是有必要写的。。
GitHub: https://github.com/facebook/fresco
官方文档: https://www.fresco-cn.org/docs/index.html
如果你的应用对图片的显示、加载等要求高的话,那就建议使用Fresco。
最大的优势在于5.0以下(最低2.3)的bitmap加载。在5.0以下系统,Fresco将图片放到一个特别的内存区域(Ashmem区)
大大减少OOM(在更底层的Native层对OOM进行处理,图片将不再占用App的内存)
适用于需要高性能加载大量图片的场景
GitHub上有很多,我自己也封装过一个Android图片选择器PhotoPicker。实现图片选择器不外乎2点:图片加载库+系统api。
一提到json解析,马上想到Google的Gson库和阿里巴巴的fastjson库,各有优点。
没有性能要求,可以使用google的Gson,如果有性能上面的要求可以使用Gson将bean转换json确保数据的正确,使用FastJson将Json转换Bean。
SAX、Pull、Dom
屏幕尺寸
手机对角线的物理尺寸 单位:英寸(inch),1英寸=2.54cm。常见5寸、5.5寸、6寸。
屏幕分辨率
手机在横向、纵向上的像素点数总和。常见320x480、480x800、720x1280、1080x1920、1080*2340。
px、dp、dpi、ppi、density
ppi等于或约等于dpi。ppi是屏幕的物理参数,叫像素密度,每英寸上像素数目。
dpi也叫像素密度。与ppi不同,dpi能被人为调整。系统指定单位尺寸上像素数量。
有几部手机分辨率相同,但是尺寸不同,他们的dpi相同,但是ppi不同。
密度density,值为dpi/160
dp是个抽象单位
density=dpi/160
dp*density=px
除了适配方案,在平时写代码的时候也需要注意,比如不要使用绝对布局,多使用百分比布局;使用.9图;使用约束布局;
此部分知识链接全来自“极客时间”,张绍文大佬的课程《Android开发高手课》,这是我买的最值的课。
https://time.geekbang.org/column/article/70602
https://time.geekbang.org/column/article/71277
https://time.geekbang.org/column/article/71277
https://time.geekbang.org/column/article/73651
https://time.geekbang.org/column/article/74988
https://time.geekbang.org/column/article/76677
https://time.geekbang.org/column/article/77990
为减少流量消耗,可采用缓存策略。常用的缓存算法是LRU(Least Recently Used):当缓存满时, 会优先淘汰那些近期最少使用的缓存对象。主要是两种方式:
LruCache 类是一个线程安全的泛型类:内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,并提供get和put方法来完成缓存的获取和添加操作,当缓存满时会移除较早使用的缓存对象,再添加新的缓存对象。
通过将缓存对象写入文件系统从而实现缓存效果。
https://time.geekbang.org/column/article/79642
https://time.geekbang.org/column/article/80921
https://time.geekbang.org/column/article/81202
概念:是将一个APP分成多个module,每个module都是一个组件,也可以是一个基础库供组件依赖,开发中可以单独调试部分组件,组件中不需要相互依赖但是可以相互调用,最终发布的时候所有组件以lib的形式被主APP工程依赖打包成一个apk。
优势
在工程build.gradle加一个开关控制moudle作为lib被主APP依赖还是作为独立APP
ext {
//true 每个业务Module可以单独开发
//false 每个业务Module以lib的方式运行
isModule = false
}
在moudle的build.gradle中
if (Boolean.valueOf(rootProject.ext.isModule)) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
//...省略
android {
//...省略
sourceSets {
main {
if (Boolean.valueOf(rootProject.ext.isModule)) {
//新建配置文件module/AndroidManifest.xml,Moudle作为独立APP时使用
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
java {
//全部module一起编译时,剔除module目录
exclude '**/module/**'
}
}
}
}
}
https://blog.csdn.net/github_37130188/article/details/89762543
https://juejin.im/post/5b0c35996fb9a009d070c081
四大组件的插件化是插件化技术的核心知识点。
插件化是体现在功能拆分方面的,它将某个功能独立提取出来,独立开发,独立测试,再插入到主应用中。以此来减少主应用的规模。插件化只是增加新的功能类或者是资源文件,所以不涉及抢先加载旧的类这样的使命,就避过了阻止相关类去打上CLASS_ISPREVERIFIED标志和还有在热修复时动态改变BaseDexClassLoader对象间接引用的dexElements.
HostApp:壳app
PluginApp:插件app
热修复是体现在bug修复方面的,它实现的是不需要重新发版和重新安装,就可以去修复已知的bug。热修复因为是为了修复Bug的,所以要将新的同名类替代同名的Bug类,要抢先加载新的类而不是Bug类,所以多做两件事:在原先的app打包的时候,阻止相关类去打上CLASS_ISPREVERIFIED标志,还有在热修复时动态改变BaseDexClassLoader对象间接引用的dexElements,这样才能抢先代替Bug类,完成系统不加载旧的Bug类
关键词:gradle文件配置、母包、子包
跨平台开发以后肯定会是主流,实现跨平台的框架与很多,各有优势缺点。毕竟环境比较复杂,考虑的要素也多。
移动互联网的热潮的兴起的时候,许多公司为了降低开发成本和抢占移动移动应用市场。市场需要技术,Hybird App应运而生。
APP经历了从本地化应用(Native App),到基于WEB的应用Web App,再到混合型应用(Hybrid APP)的过程。
Hybrid App(混合型应用)是指介于web-app、native-app这两者之间的app,Hybrid app从外观上来看是一个native app(因为有用到native的壳),内部又是使用web-app,如新闻类和视频类的应用普遍采取该策略:native的框架加上web的内容。不同于native app需要针对不同的平台使用不同的开发语言(如使用Objective-C、Swift开发iOS应用,使用Java、Kotlin开发Android应用,使用C#开发Windows Phone应用),hybrid app允许开发者仅使用一套网页语言代码,即可开发能够在不同平台上部署的类原生应用 (网页编程语言有很多,如HTML、JavaScript、PHP,我们需要从中选择我们需要使用的一种或几种来开发Hybrid App,比如HTML5+JavaScript)。
Hybrid App的本质,其实是在原生的 App 中,使用 WebView 作为容器直接承载 Web页面。因此,最核心的点就是 Native端 与 H5端 之间的双向通讯层,其实这里也可以理解为我们需要一套跨语言通讯方案,来完成 Native(Java/Objective-c/…) 与 JavaScript 的通讯。这个方案就是我们所说的 JSBridge,而实现的关键便是作为容器的 WebView,一切的原理都是基于 WebView 的机制。
Hybird有很多种分类,多View和单View混合型是偏向于原生应用的,通常不能跨平台;而Web主体型和复合型是偏向于网页应用的,可以跨平台。
补充
Web App(网页应用) | Hybrid App(混合应用) | Native App(原生应用) | |
---|---|---|---|
开发成本 | 低 | 中 | 高 |
维护更新 | 简单 | 简单 | 复杂 |
体验 | 差 | 中 | 优 |
Store或market认可 | 不认可 | 认可 | 认可 |
安装 | 不需要 | 需要 | 需要 |
跨平台支持 | 支持 | 一部分支持 | 不支持 |
开发语言 | 只使用web编程语言 | 原生开发语言+web | 只使用原生开发语言 |
Apache Cordova是一个开源的移动开发框架。允许你用标准的web技术-HTML5,CSS3和JavaScript做跨平台开发。Cordova的命令行运行在Node.js 上面并且可以通过NPM安装。常用于Hybird App开发。
详情见:Cordovaz中文网
GitHub地址:https://github.com/facebook/react-native
Flutter官方中文网
Flutter已经和Dart语言绑定了。Dart 是目前唯一一个支持严格的 AOT 和 JIT 的编程语言。
Flutter非常火,有兴趣可以看看2019GMTC全球大前端技术大会上任晓帅的演讲《Dart is All The Things》。
可以看一下 2019GMTC全球大前端技术大会上李思广的演讲《多端一体化框架 Hippy 的开放与未来》。
为啥要配置环境变量,很简单,不配置就只能在Android Studio里的terminal里使用adb命令,配置后你可以在任意文件夹里通过调出命令提示符来使用adb命令。
查看版本号
显示当前运行的全部模拟器
安装当前路径下的xx.apk
adb uninstall
获取模拟器中的文件
向模拟器中写文件(确保文件系统不是 Read-only file system,不然push会失败)
进入shell模式
进入shell模式键入cd data/app,然后通过“ls //”查看文件列表,部分shell是Permission denied。
1、你的手机是root过的或者是一台这样的机器.(ADP1 or an ION from Google I/O)
2、以root模式运行adb,输入adb root.
#cd system/sd/data //进入系统内指定文件夹
#ls //列表显示当前文件夹内容
#rm -r xxx //删除名字为xxx的文件夹及其里面的所有文件
#rm xxx //删除文件xxx
#rmdir xxx //删除xxx的文件夹
查看分辨率
在命令行中查看LOG信息
debug模式
通过命令打开某个Activity如果报错:Security exception: Permission Denial
那就要去配置文件里看,这个Activity有没有intent-filter属性。Android组件中有个exported属性,当组件没有intent-filter时exported属性默认为false,此组件只能由本应用用户访问,配备了intent-filter后此值改变为true,允许外部调用。
获取设备ID
获取设备序列号
组件只管依赖的使用,而依赖的具体实现交给容器去决定,这是DI(Dependency Injection)框架的核心思想。
参考项目:google/dagger
使用Dagger2不用担心性能的消耗,不使用反射,所有的注解均停留在编译时期。
使用 Room 将数据保存到本地数据库
greendao使用记录
早期的mvvm架构项目,基本使用的databinding。直到后来出现jetpack之后,mvvm实现能借助的工具有了更多选择。
可查看我写的:databinding实践
Gradle是一个构建工具,它是用来帮助我们构建app的,构建包括编译、打包等过程。
Gradle 是基于groovy语言实现(基于JVM的语法和java类似的脚本语言)的一个Android编译系统, google针对Android编译用groovy语言开发了一套dsl,这就是gradle。 因此,遇到不明白的gradle配置,直接看看相关groovy的源码,一般都可以找到解决的办法。
2018年谷歌I/O 发布了一系列辅助android开发者的实用工具,合称Jetpack,以帮助开发者构建出色的 Android 应用。jetpack家族很庞大,值得深入研究。
Jetpack MVVM 的边界目前仅限于 LifeCycle、LiveData、ViewModel、DataBinding 这四样。
扔几篇好文:
重学安卓:从 被误解 到 真香 的 Jetpack DataBinding!
重学安卓:有了 Jetpack ViewModel . . . 真的可以为所欲为!
重学安卓:是让人耳目一新的 Jetpack MVVM 精讲啊!
JNI,全称为Java Native Interface,即Java本地接口,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植。
JNI使用记录
JNI装载库文件load和loadLibrary浅析
关于NDK如何生成so文件
响应式编程、链式调用,rxjava是个方便的东西。学习rxjava的关键在使用操作符。操作符的本质是声明式编程。与命令式不一样的是,声明式编程只交待做什么,但无须交待怎么做。有点像SQL语句,按一定的规则,向数据库中的数据声明你要做什么(增删改查)。
rxjava也该这么学习,对于各类操作符,我们只需要关心输入数据、变换规则和输出数据。
//关于map是如何变换数据,filter是如何限制数据,我们无需关心
Observable.just(1, 3, 5, 7, 9)
.map(i -> i + 1)
.filter(i -> i < 5)
.subscribe(getObserve());
RxJava2专栏
RxJava1系列文章开篇
apktool,dex2jar,jd-gui简单使用
apk解包修改后重新打包
Robotium、Robolectric等测试框架;
Google的UI测试框架Espresso;
架构范围最大
框架与架构
架构比较抽象,框架比架构更具体,更偏向于实现某种“方便”,1个架构可以通过使用多种框架来实现。
框架与设计模式
1种设计模式可以被不同框架和不同语言实现,框架是多种设计模式和代码的混合体。
架构与设计模式
设计模式是用于解决一种特定的问题,范围较小;架构针对体系结构进行设计,范畴较大。一个架构里可能有多个框架和多个设计模式,为的都是让框架更加稳定。
MVC、MVP、MVVM
MVP 和 MVVM 二者之间没有任何关系。MVP 的出现与MVC离不开(为了解决MVC的一些问题,是MVC的升级),而 MVVM 是现代化软件开发模式的范例。
关键字:视图、模型、业务逻辑处理、数据逻辑处理、界面交互、视图事件。
不使用架构进行开发,带来的问题是 各活动窗口及碎片逻辑臃肿,不利于扩展。MVC作用就是解耦。减少控制逻辑、数据逻辑和界面交互的耦合度。
作用也是解耦,切断了View和Model之间的联系,用Presenter充当桥梁。MVP是基于适配器模式,在MVC 模式泛滥的背景下,遵循依赖倒置原则以便能够随时替换 V 和 M 的一种实现。
优点:
缺点
会引入大量的接口,导致项目文件数量激增;P层里的代码量会比较大,P持有M层和V层引用对象,M和V发生改变(体现在接口的变化)时,需要同步。需要注意异步调用情况下,页面关闭调用View层方法空指针等。
作用也是解耦。同时将 MVC 中的 View 和 Model 解耦以及 MVP 中 Presenter 和 View 解耦。
ViewModel 也不会持有 View。其中 ViewModel 中的改动,会自动反馈给 View 进行界面更新,而 View 中的事件,也会自动反馈给 ViewModel。ViewModel只负责状态变量本身的变化,其他的不管(比如变量被哪些视图绑定,有没有绑定等)。
通常需要借助工具辅助来实现,比较常用的就是 databinding。
此外Jetpack的出现,让MVVM有了更多的选择。
重学安卓:Activity 的快乐你不懂!
Android 操作系统架构开篇
Android系统架构与系统源码目录
写给 Android 应用工程师的 Binder 原理剖析
Activity的启动模式
任务和返回栈
代码混淆到底做了什么?
Android 开发中的架构模式