刚入行安卓的萌新一枚,目前对工作内容,工作方向还处于混沌状态,搞个Android Studio和一个简单的app都搞了四天,期间的一些问题也缺少一套方法论去解决,完全是面向百度,没有解决问题的思路,写个博客总结一下这几天的一些坑和遇到的问题。用一个写的乱七八糟的demo去由浅入深稍微熟悉一下安卓四大组件,以及一个项目从开发到打包成apk的过程。
本Demo写的时候属于是意识流写法,并没有明确的思路(软件工程还给老师了),现在重新学着规范,写一下文档。
实际上是自己临时想的一些功能,似乎网上也有比较完善的实现代码。最初的设想是能具备以下功能:
选择完项目之后的第一步,就是可行性分析。可行性分析包含四个要点:技术可行性,经济可行性,操作可行性,法律可行性。
实现音乐播放部分的很简单,基本上就是MusicActivity和MyService实现的。MusicActivity利用广播对Myservice的行为进行控制。MyService实际上对音乐进行播放暂停等操作,由于实际的显示界面是由MusicActiviy展示的,所以如果需要对歌曲信息等进行展示,还需要MysService的广播将自己的歌曲信息以及状态反馈给MusicActivity。其具体实现过程放到后文讲。这一部分是音乐播放的功能,没有涉及到数据库操作,希望后续熟悉后能加入。这是音乐播放部分。
此外虽然没有完成但是依然保留的定时启动钉钉部分,主界面的按钮可以跳转到ClockActivity,具体是通过安卓自带的TimePicker来实现闹钟定时功能,然后通过pingIntent来传输给静态广播需要打开某app的请求。由于不知名原因,定时和启动功能均未能正确运行。
至此,安卓四大组件其三以及提到过了,分别是:
在讲四大组件前先来看一下安卓架构
以下许多内容都是参考大佬的博客[点击跳转]干货很多(https://blog.csdn.net/joye123/article/details/116197862?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_utm_term~default-1-116197862-blog-91946872.pc_relevant_multi_platform_whitelistv3&spm=1001.2101.3001.4242.2&utm_relevant_index=4)
可以看到四大组件是属于应用程序框架层。而开发者进行安卓app开发基本上都是基于四大组件进行开发。
Activity:Activity是开发中最常用的,也是最复杂的一个组件。它是用户可以专注做一些事情的东西。它的主要功能就是可以和用户进行交互操作,所以几乎所有的Activity都会负责创建一个显示窗口,然后通过setContentView显示特定的UI。
Service:除了Activity,Service是第二复杂的组件。和Activity相比,Service是一种处于后台长时间运行的组件,它没有UI界面,不需要与用户交互。它被设计用来后台执行耗时任务或者为其他应用程序提供功能调用的服务。
BroadcastReceiver:广播接收者,这个组件比较简单,比较好理解了。类似于观察者模式,应用程序可以添加自己关心的广播事件,从而为用户提供更好的使用体验。这些广播可以是来自于操作系统、其他的应用程序、甚至是自己的应用程序。例如网络连接变化、电池电量变化、开机启动等。
ContentProvider:内容提供者,它被设计用来在不同的应用程序之间共享数据。例如电话程序中的联系人数据,就可以被其他应用程序读取。如果仅仅是在同一个程序中存取数据的话,用SQLiteDatabase接口就可以了。
除了BroadcastReceiver可以动态注册外,四大组件在使用之前必须先在 AndroidManifest.xml清单文件中进行声明。我们这个demo中ClockActivity用的就是静态注册,需要在AndroidManifest.xml中声明,即使app关闭了,仍然可以在后台工作。
/ContentProvider声明
在应用程序安装时,应用安装程序通过PackageInstaller服务解析应用安装包,并将AndroidManifest.xml中声明的四大组件信息保存到PackageManagerService中。
public class PackageManagerService extends IPackageManager.Stub
implements PackageSender {
...
//组件解析器,存储了系统所有应用程序的四大组件信息
private final ComponentResolver mComponentResolver;
}
public class ComponentResolver {
/** All available activities, for your resolving pleasure. */
@GuardedBy("mLock")
private final ActivityIntentResolver mActivities = new ActivityIntentResolver();
/** All available providers, for your resolving pleasure. */
@GuardedBy("mLock")
private final ProviderIntentResolver mProviders = new ProviderIntentResolver();
/** All available receivers, for your resolving pleasure. */
@GuardedBy("mLock")
private final ActivityIntentResolver mReceivers = new ActivityIntentResolver();
/** All available services, for your resolving pleasure. */
@GuardedBy("mLock")
private final ServiceIntentResolver mServices = new ServiceIntentResolver();
...
}
另外一个重要的类是Context
Context是关于应用程序环境的全局信息接口,Context是一个抽象类,它的实现都是由系统类实现的。Context允许访问应用程序特定的资源和类,如
资源管理器AssetsManager、
包管理器PackageManager、
文本图片主题资源Resource、
主线程消息循环Looper
四大组件操作,如启动Activity、BroadcastReceiver,接收Intent
SharedPreferences操作
私有目录文件操作
数据库创建、删除操作
获取系统服务
权限操作
如图,四大组件中的Activity和Service都是Context下的子类,Context
//android.content.Context
public abstract class Context {
public abstract AssetManager getAssets();
public abstract Resources getResources();
public abstract PackageManager getPackageManager();
public abstract ContentResolver getContentResolver();
public abstract Looper getMainLooper();
public final CharSequence getText(@StringRes int resId) {
return getResources().getText(resId);
}
public final String getString(@StringRes int resId) {
return getResources().getString(resId);
}
public abstract String getPackageName();
}
Context的实现类为ContextImpl,ContextImpl为Activity或其他应用程序组件提供了基础的Context实现。
//android.app.ContextImpl
class ContextImpl extends Context {
//ContextImpl的构造方法是私有的,只能通过几个静态方法创建ContextImpl实例
private ContextImpl(@Nullable ContextImpl container,
@NonNull ActivityThread mainThread,
@NonNull LoadedApk packageInfo,
@Nullable String splitName,
@Nullable IBinder activityToken,
@Nullable UserHandle user, int flags,
@Nullable ClassLoader classLoader,
@Nullable String overrideOpPackageName) {
...
}
....
//创建系统应用的上下文
@UnsupportedAppUsage
static ContextImpl createSystemContext(ActivityThread mainThread) {
LoadedApk packageInfo = new LoadedApk(mainThread);
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0, null, null);
context.setResources(packageInfo.getResources());
context.mResources.updateConfiguration(context.mResourcesManager.getConfiguration(), ontext.mResourcesManager.getDisplayMetrics());
return context;
}
//基于系统应用Context创建的用于UI的系统上下文,此上下文具有可以主题化的资源
static ContextImpl createSystemUiContext(ContextImpl systemContext,
int displayId) {
final LoadedApk packageInfo = systemContext.mPackageInfo;
ContextImpl context = new ContextImpl(null, systemContext.mMainThread,
packageInfo, null,null, null, 0, null, null);
context.setResources(createResources(null, packageInfo, null, displayId,
null,packageInfo.getCompatibilityInfo()));
context.updateDisplay(displayId);
return context;
}
//创建普通应用级别的上下文,packageInfo指定了某个已安装的应用
static ContextImpl createAppContext(ActivityThread mainThread,
LoadedApk packageInfo,
String opPackageName) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
ContextImpl context = new ContextImpl(null, mainThread, packageInfo,
null, null, null, 0,
null, opPackageName);
context.setResources(packageInfo.getResources());
return context;
}
//创建Activity级别的上下文
static ContextImpl createActivityContext(ActivityThread mainThread,
LoadedApk packageInfo,
ActivityInfo activityInfo,
IBinder activityToken, //代表一个Activity
int displayId,
Configuration overrideConfiguration) {
if (packageInfo == null)
throw new IllegalArgumentException("packageInfo");
String[] splitDirs = packageInfo.getSplitResDirs();
ClassLoader classLoader = packageInfo.getClassLoader();
if (packageInfo.getApplicationInfo().requestsIsolatedSplitLoading()) {
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "SplitDependencies");
try {
classLoader=packageInfo.getSplitClassLoader(activityInfo.splitName);
splitDirs = packageInfo.getSplitPaths(activityInfo.splitName);
} catch (NameNotFoundException e) {
// Nothing above us can handle a NameNotFoundException, better crash.
throw new RuntimeException(e);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
}
}
ContextImpl context = new ContextImpl(null, mainThread, packageInfo,
activityInfo.splitName,activityToken, null, 0, classLoader, null);
// Clamp display ID to DEFAULT_DISPLAY if it is INVALID_DISPLAY.
displayId = (displayId != Display.INVALID_DISPLAY) ? displayId :
Display.DEFAULT_DISPLAY;
final CompatibilityInfo compatInfo = (displayId ==
Display.DEFAULT_DISPLAY)
? packageInfo.getCompatibilityInfo()
: CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO;
final ResourcesManager resourcesManager =ResourcesManager.getInstance();
// Create the base resources for which all configuration contexts for this Activity
// will be rebased upon.
context.setResources(resourcesManager.createBaseActivityResources(
activityToken,
packageInfo.getResDir(),
splitDirs,
packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles,
displayId,
overrideConfiguration,
compatInfo,
classLoader));
context.mDisplay = resourcesManager.getAdjustedDisplay(displayId,
context.getResources());
return context;
}
...
}
除了ContextImpl类外,ContextWrapper类也继承了Context,但是ContextWrapper类并不自己实现了Context的方法,而是通过构造方法,代理给另外一个Context的实现。这样ContextWrapper的子类就可以在不修改ContextWrapper类的情况下,修改其调用方法的实现
public class ContextWrapper extends Context {
Context mBase;
public ContextWrapper(Context base) {
mBase = base;
}
protected void attachBaseContext(Context base) {
if (mBase != null) {
throw new IllegalStateException("Base context already set");
}
mBase = base;
}
...
}
这里使用了代理模式,因为base实际上是Context类,真正最后调用的是ContextImpl的实现方法。
一个新的应用启动时,会优先初始化其Application类,创建了Application实例后,会立即调用其attach方法。
然后就会初始化应用中声明的ContentProvider:
ContentProvider初始化完成后,会再调用Application类的onCreate方法。
AMS在初始化完客户端的Application类后,会检查是否有需要运行的Service和BroadcastReceiver。
所以初始化顺序是Application.attach() > ContentProvider> Application.onCreate() > Activity/Service/BroadcastReceiver。
ContentProvider比Activity或Service初始化的顺序都要早,所以有些第三方库利用这个特性,通过ContentProvider自动初始化一些功能,而不用在Application中添加初始化代码。
protected void onCreate(Bundle savedInstanceState);
Activity创建,用于初始化数据
protected void onStart();
Activity UI可见
protected void onRestart();
Activity UI从不可见变为可见
protected void onResume();
Activity UI可操作
protected void onPause();
Activity 暂停,UI可见,不可操作
protected void onStop();
Activity停止,UI不可见
protected void onDestroy();
Activity销毁
Service是一种应用程序组件,用于两种使用场景:
后台执行长时间的任务,而不需要与用户交互。例如后台播放音乐
将本应用的功能通过接口的形式暴露给其他应用程序调用
两种启动Service的方式:
1、startService,Service会执行onCreate和onStartCommand。多次启动同一个Service不会多次执行onCreate,会多次执行onStartCommand
2、bindService,Service会执行onBind方法,返回给调用者一个Binder对象,用于功能接口调用。
Service优先级:
Service所在进程的优先级仅次于前台进程的优先级,系统会尽量避免杀死该进程,除非内存压力非常大。如果被系统杀死了,系统会稍后尝试重启该Service,并重新将Intent数据发送Service来恢复杀死之前的状态。
onStartCommand方法的返回值,决定Service被系统杀死后的操作
START_NOT_STICKY 被系统杀死后不会被重启
START_STICKY 被系统杀死后会重建,但是会发送一个null给onStartCommand
START_REDELIVER_INTENT 被系统杀死后会重建,并且会逐一发送等待处理的Intent给onStartCommand
创建Service实例
调用Service的attach方法
调用Service的onCreate方法
以token为key保存到Map中,方便后续多次调用onStartCommand
告知AMS启动完成
bindService与startService不同的地方是,bindService需要提供一个ServiceConnection的回调接口,用来告知调用者已连接或断开连接。
多次调用startService
执行一次Service的onCreate
多次执行Service的onStartCommand
多次调用bindService
执行一次Service的onCreate
执行一次Service的onBind
调用方的ServiceConnection也只会回调一次
ContentProvider用于在不同应用程序间提供数据,它在内部实现了跨进程的调用,不需要数据提供者或调用者关心。
调用者通过URI向ContentProvider查询数据。
一般是调用ContentResolver来获取数据
context.getContentResolver().query("查询的ContentProviderUri", "返回的列", "过滤行的条件", "过滤行的参数", "返回行数据的排序方式");
在Manifesh.xml文件中注册自定义的BroadcastReceiver,当意图过滤器中的动作发生时,会回调BroadcastReceiver中的onRecevie方法.
静态注册
动态注册
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent == null ? "" : intent.getAction();
if (Intent.ACTION_SCREEN_OFF.equalsIgnoreCase(action)) {
Log.i(TAG, "onReceive: 屏幕关闭");
}
context.unregisterReceiver(this);
}
}, intentFilter);
在Activity上下文中调用registerReceiver方法,动态注册一个广播接收者,同时指定了意图过滤器。
。
项目目录解释
这种形式来获得这个按钮把它包装成一个对象
package com.example.note;
import androidx.appcompat.app.AppCompatActivity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.widget.Button;
import android.widget.Toast;
public class MusicActivity extends AppCompatActivity {
Button last, start, next;
public static final String CTRL_ACTION = "com.example.note.CTRL";
public static final String UPDATE_ACTION="com.example.note.UPDATE";
int status = 0x11;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_music);
last = findViewById(R.id.up);
start = findViewById(R.id.start);
next = findViewById(R.id.down);
Intent intent = new Intent(MusicActivity.this, MyService.class);
ActivityReceiver activityReceiver = new ActivityReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(UPDATE_ACTION);
registerReceiver(activityReceiver, filter);
startService(intent);
last.setOnClickListener(view -> {
Intent intent1=new Intent(CTRL_ACTION);
intent1.putExtra("control", 3);
this.sendBroadcast(intent1);
});
start.setOnClickListener(view -> {
Intent intent1=new Intent(CTRL_ACTION);
intent1.putExtra("control", 1);
sendBroadcast(intent1);
});
next.setOnClickListener(view -> {
Intent intent1=new Intent(CTRL_ACTION);
intent1.putExtra("control", 4);
sendBroadcast(intent1);
});
}
private class ActivityReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
//获取来自receive中intent的update消息,代表播放状态
int update = intent.getIntExtra("update", -1);
switch (update) {
//播放歌曲
case 0x11:
status = 0x11;
break;
//播放>暂停
case 0x12:
status = 0x12;
break;
}
}
}
}
在onCreate方法里先启动MyService
Intent intent = new Intent(MusicActivity.this, MyService.class);
startService(intent);
分别绑定了三个按钮,并且每个按钮设置的监听事件是发送一个广播
next.setOnClickListener(view -> {
Intent intent1=new Intent(CTRL_ACTION);
intent1.putExtra("control", 4);
sendBroadcast(intent1);
});
例如这个点击下一首的通过发送控制信息给Myservice的内部广播类来进行切歌的控制。
MyService类是另一个重要的实现类
package com.example.note;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.media.MediaPlayer;
import android.os.IBinder;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.Nullable;
import com.example.note.receiver.MyReceiver;
import java.io.IOException;
public class MyService extends Service {
@Nullable
MyReceiver serviceReceiver;
AssetManager am;
String[] musics=new String[]{"music0.mp3","music1.mp3","music2.mp3","music3.mp3","music4.mp3"};
MediaPlayer mPlayer;
//0x11表示没有播放,0x12代表正在播放,0x13代表暂停
int status=0x11;
int current=0;
public IBinder onBind(Intent intent) {
return null;
}
public void onCreate(){
super.onCreate();
am=getAssets();
//创建BroadcastReceiver
serviceReceiver=new MyReceiver();
//创建IntentFilter
IntentFilter filter=new IntentFilter();
filter.addAction(MusicActivity.CTRL_ACTION);
registerReceiver(serviceReceiver,filter);
mPlayer=new MediaPlayer();
//为MediaPlayer播放完成事件绑定监听器
mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
current++;
if (current>=musics.length)
{
current=0;
}
Intent sendIntent = new Intent(MusicActivity.UPDATE_ACTION);
sendIntent.putExtra("current",current);
//发送广播,将被Activity组件中的BroadcastReceiver接收
sendBroadcast(sendIntent);
//准备播放音乐
prepareAndPlay(musics[current]);
}
});
}
private class MyReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
int control =intent.getIntExtra("control",-1);
switch (control)
{
//播放或暂停
case 1:
//原来处于没有播放状态
if (status==0x11)
{
//准备并播放音乐
prepareAndPlay(musics[current]);
status=0x12;
}
//原来处于播放状态
else if (status==0x12)
{
//暂停
mPlayer.pause();
//改变为暂停状态
status=0x13;
}
//原来处于暂停状态
else if (status==0x13)
{
//播放
mPlayer.start();
//改变状态
status=0x12;
}
break;
//停止声音
case 2:
//如果原来正在播放或暂停
if (status==0x12||status==0x13) {
//停止播放
mPlayer.stop();
status = 0x11;
}
break;
case 3:
//原来处于没有播放或暂停状态
if (status==0x11||status==0x13)
{
if(current==0) {
current=4;
prepareAndPlay(musics[current]);
}
//准备并播放音乐
else {
current=current-1;
prepareAndPlay(musics[current]);
}
status=0x12;
}
//原来处于播放状态
else if (status==0x12)
{
//上一首//准备并播放音乐
if(current==0) {
current=4;
prepareAndPlay(musics[current]);
}
else {
current=current-1;
prepareAndPlay(musics[current]);
}
}
break;
case 4:
//原来处于没有播放或暂停状态
if (status==0x11||status==0x13)
{
if(current==4) {
current=0;
prepareAndPlay(musics[current]);
} //准备并播放音乐
else {
current=current+1;
prepareAndPlay(musics[current]);
}
status=0x12;
}
else if (status==0x12)
{
if(current==4) {
current=0;
prepareAndPlay(musics[current]);
}
else {
current=current+1;
prepareAndPlay(musics[current]);
}
}
break;
}
//广播通知Activity更改图标、文本框
Intent sendIntent=new Intent(MusicActivity.UPDATE_ACTION);
sendIntent.putExtra("update",status);
sendIntent.putExtra("current",current);
//发送广播,将被Activity组件中的BroadcastReceiver接收
sendBroadcast(sendIntent);
}
}
private void prepareAndPlay(String music)
{
try
{
//打开指定音乐文件
AssetFileDescriptor afd=am.openFd(music);
mPlayer.reset();
//使用MediaPlayer加载指定的音乐文件
mPlayer.setDataSource(afd.getFileDescriptor(),afd.getStartOffset(),afd.getLength());
//准备声音
mPlayer.prepare();
//播放
mPlayer.start();
Toast.makeText(this,music+"正在播放",Toast.LENGTH_SHORT).show();
}catch (IOException e) {
e.printStackTrace();
}
}
}
这个类里面自己写的时候踩得雷基本上就是MediaPlayer的调用问题,对这个类的api了解不够导致总是出一些异常。姑且不谈
这里我们5首音乐全部放在assets里而非raw里,调用asserts需要通过AssetManage来调用
这个服务里也用到了广播组件,不过这里主要用来接收控制请求,和所有其他组件一样,都要进行注册,这里用了动态注册的方法
IntentFilter filter=new IntentFilter();
filter.addAction(MusicActivity.CTRL_ACTION);//在MusicActivity中定义的静态变量,也是这个广播被识别调用的重要标识,相当于一个身份证。
registerReceiver(serviceReceiver,filter);
本demo中没有用到content provider,之后会考虑添加应用场景。
除了四大组件,我们可以看到还有一个很重要的类Intent类。
Intent用于启动Activity, Service, 以及BroadcastReceiver三种组件, 同时还是组件之间通信的重要媒介。
在demo里,可以看到,用广播传递控制信息,接收广播,开启服务,开启活动所用的都是Intent。
更多内容之后在研究吧。
本demo中熟悉了一下三大组件的一些使用,但是代码没有很好的设计,看起来不够清晰。比如music
活动类还可以使用继承Onclicklistender的方式来重写onclick方法,对所有按钮的响应事件写进一个方法中。还有初版需求没有完成留下的没有用的界面,以后在写代码前要先思考整体的框架以及各个模块的功能,然后填充代码。