第013天:Android开发的高级技巧介绍_android 高级技术-程序员宅基地

技术标签: java  android  手机app开发实战  开发语言  

        本书的内容虽然已经接近尾声了,但是千万不要因此而放松,现在正是你继续进阶的时机。 相信基础性的Android知识已经没有太多能够难倒你的了,那么本章中我们就来学习一些你还应该掌握的高级技巧吧。

13.1全局获取Context的技巧

        回想这么久以来我们所学的内容,你会发现有很多地方都需要用到Context,弹出Toast的时候需要,启动活动的时候需要,发送广播的时候需要,操作数据库的时候需要,使用通知的时候需要,等等等等。

        或许目前你还没有为得不到Context而发愁过,因为我们很多的操作都是在活动中进行的, 而活动本身就是一个Context对象。但是,当应用程序的架构逐渐开始复杂起来的时候,很多的逻辑代码都将脱离Activity类,但此时你又恰恰需要使用Context,也许这个时候你就会感到有些伤脑筋了。

        举个例子来说吧,在第9章的最佳实践环节,我们编写了一个HttpUtil类,在这里将一些 通用的网络操作封装了起来,代码如下所示:

public class HttpUtil {
    public static void sendHttpRequest(final String address, final HttpCallbackListener     
    listener) { new Thread(new Runnable() {
    @Override public void run() { HttpURLConnection connection = null; try {
    URL url = new URL(address);
    connection = (HttpURLConnection) url.openConnection();         
    connection.setRequestMethod("GET");
    connection.setConnectTimeout(8000);
    connection.setReadTimeout(8000);
    connection.setDoInput(true);
    connection.setDoOutput(true);
    Inputstream in = connection.getInputStream(); BufferedReader reader = new     
    BufferedReader(new InputStreamReader(in));
    StringBuilder response = new StringBuilder(); String line;
    while (dine = reader. readLine()) != null) { response.append(line);
        )
    if (listener != null) {
            //回调onFinish()方法 listener.onFinish(response.toString());
                }
        } catch (Exception e) {
            if (listener != null) {
                //回调onE「ror()方法 listener.onError(e);
                                }
                        } finally {
                if (connection != null) { connection.disconnect();
                            }
                                                }
            }
            }).start();
}

        这里使用sendHttpRequestO方法来发送HTTP请求显然是没有问题的,并且我们还可以在回调方法中处理服务器返回的数据。但现在我们想对sendHttpRequestO方法进行一些优化, 当检测到网络不存在的时候就给用户一个Toast提示,并且不再执行后面的代码。看似一个挺简单的功能,可是却存在一个让人头疼的问题,弹出Toast提示需要一个Context参数,而我们在HttpUtil类中显然是获取不到Context对象的,这该怎么办呢?

        其实要想快速解决这个问题也很简单,大不了在sendHttpRequestO方法中添加一个 Context参数就行了嘛,于是可以将HttpUtil中的代码进行如下修改:

public class HttpUtil {
    public static void sendHttpRequest(final Context context, final String address, final         
    HttpCallbackListener listener) { if (:isNetworkAvailable()) {
    Toast.makeText(contextf "network is unavailable", Toast・ LENGTH_SHORT).show();
    return;
    }
    new Th read(new Runnable() {
    @Override
    public void run() {
    }
    }).start();
        }
    private static boolean isNetworkAvailable() {
}
}

        可以看到,这里在方法中添加了一个Context参数,并且假设有一个isNetwo rkAvailable () 方法用于判断当前网络是否可用,如果网络不可用的话就弹岀Toast提示,并将方法return掉。

        虽说这也确实是一种解决方案,但是却有点推卸责任的嫌疑,因为我们将获取Context的 任务转移给了 sendHttpRequestO方法的调用方,至于调用方能不能得到Context对象,那就 不是我们需要考虑的问题了。

        由此可以看出,在某些情况下,获取Context并非是那么容易的一件事,有时候还是挺伤 脑筋的。不过别担心,下面我们就来学习一种技巧,让你在项目的任何地方都能够轻松获取到 Contexto

        Android提供了一个Application类,每当应用程序启动的时候,系统就会自动将这个类进 行初始化。而我们可以定制一个自己的Application类,以便于管理程序内一些全局的状态信 息,比如说全局Contexto

        定制一个自己的Application其实并不复杂,首先我们需要创建一个MyApplication类继 承自Application,代码如下所示:

public class MyApplication extends Application {
    private static Context context;
    @Override
    public void onCreate() {
    context = getApplicationContext();
    }
    public static Context getContext() { return context;
}
)

        可以看到,MyApplication中的代码非常简单。这里我们重写了父类的onCreate()方法, 并通过调用getApplicationContext()方法得到了一个应用程序级别的Context,然后又提供 了一个静态的getContext ()方法,在这里将刚才获取到的Context进行返回。

        接下来我们需要告知系统,当程序启动的时候应该初始化MyApplication类,而不是默认 的Application类。这一步也很简单,在AndroidManifest.xml文件的<application>标签下进 行指定就可以了,代码如下所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.networktest"
    android:versionCode="l"
    android:versionName="l.0" >
    <application
    android: name=,lcom . example ・ networktest. MyApplication"
    ...>
    </application>
</manifest>

        注意这里在指定MyApplication的时候一定要加上完整的包名,不然系统将无法找到这 个类。

        这样我们就已经实现了一种全局获取Context的机制,之后不管你想在项目的任何地方使 用 Context,只需要调用一下 MyApplication.getContext()就可以了。

        那么接下来我们再对sendHttpRequestO方法进行优化,代码如下所示:

public static void sendHttpRequest(final String address, final HttpCallbackListener     
    listener) { if (!isNetworkAvailable()) {
    Toast.makeText(MyApplication.getContext(), "network is unavailable",
    Toast.LENGTHSHORT).show();
    return;
}
}"

        可以看到,sendHttpRequest方法不需要再通过传参的方式来得到Context对象,而是 调用一下MyApplication.getContext ()方法就可以了。有了这个技巧,你再也不用为得不到 Context对象而发愁了。

        然后我们再回顾一下6.5.2小节学过的内容,当时为了让LitePal可以正常工作,要求必须在 AndroidManifest.xml中配置如下内容:

<application

android:name="org.litepal.LitePalApplication"

...>

</application>

        其实道理也是一样的,因为经过这样的配置之后,LitePal就能在内部自动获取到Context 了。 不过这里你可能又会产生疑问,如果我们已经配置过了自己的Application怎么办?这样 岂不是和LitePalApplication冲突了?没错,任何一个项目都只能配置一个Application, 对于这种情况,LitePal提供了很简单的解决方案,那就是在我们自己的Application中去调用 LitePal的初始化方法就可以了,如下所示:

public class MyApplication extends Application {

private static Context context;

@override

public void onCreate() {

context = getApplicationContext();

LitePalApplication.initialize(context);

}

public static Context getContext() {

return context;

}

}

        使用这种写法,就相当于我们把全局的Context对象通过参数传递给了 LitePal,效果和在 AndroidManifest.xml 中配置 LitePalApplication 是一模一样的。

13.2使用Intent传递对象

        Intent的用法相信你已经比较熟悉了,我们可以借助它来启动活动、发送广播、启动服务等。 在进行上述操作的时候,我们还可以在Intent中添加一些附加数据,以达到传值的效果,比如在 FirstActivity中添加如下代码:

Intent intent = new Intent(FirstActivity.this, SecondActivity.class);

intent.putExt ra("st ringdata", "hello");

intent.putExtra("intdata", 100);

startActivity(intent?;

        这里调用了 IntentputExt ra ()方法来添加要传递的数据,之后在SecondActivity中就 可以得到这些值了,代码如下所示:

getlntent().getSt ringExt ra("st ring data");

getlntent().getIntExtra("intdata", 0);

        但是不知道你有没有发现,putExtra()方法中所支持的数据类型是有限的,虽然常用的一 些数据类型它都会支持,但是当你想去传递一些自定义对象的时候,就会发现无从下手。不用担 心,下面我们就学习一下使用Intent来传递对象的技巧。

  1. Serializable 方式

        使用Intent来传递对象通常有两种实现方式:SerializableParcelablc,本小节中我们先来学 习一下第一种实现方式。

        Serializable是序列化的意思,表示将一个对象转换成可存储或可传输的状态。序列化后的对 象可以在网络上进行传输,也可以存储到本地。至于序列化的方法也很简单,只需要让一个类去 实现Serializable这个接口就可以了。

        比如说有一个Person类,其中包含了 nameage这两个字段,想要将它序列化就可以这 样写:

public class Person implements Serializable{
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) (
this.name = name;
public int getAge() { return age;
}
public void setAge(int age) {
this.age = age;
}
}

        其中,get. set方法都是用于赋值和读取字段数据的,最重要的部分是在第一行。这里让

Person类去实现了 Serializable接口,这样所有的Person对象就都是可序列化的了。

接下来在FirstActivity中的写法非常简单:

Person person = new Person();

person.setName("Tom");

person.setAge(20);

Intent intent = new Intent(FirstActivity.this, SecondActivity.class);

intent.putExtra("persondata", person);

startActivity(intent);

        可以看到,这里我们创建了一个Person的实例,然后就直接将它传入到putExtraO方法 中了。由于Person类实现了 Serializable接口,所以才可以这样写。

        接下来在SecondActivity中获取这个对象也很简单,写法如下:

Person person = (Person) getlntent().getSerializableExtra("persondata");

这里调用了 getSerializableExtra()方法来获取通过参数传递过来的序列化对象,接着再 将它向下转型成Person对象,这样我们就成功实现了使用Intent来传递对象的功能了。

  1. Parcelable 方式

        除了 Serializable之外,使用Parcelable也可以实现相同的效果,不过不同于将对象进行序列 化,Parcelable方式的实现原理是将一个完整的对象进行分解,而分解后的每一部分都是Intent 所支持的数据类型,这样也就实现传递对象的功能了。

        下面我们来看一下Parcelable的实现方式,修改Person中的代码,如下所示:

public class Person implements Parcelable {

private String name;

private int age;

Override

public int describeContents() {

return 0;

}

Override

public void w riteToPa rcel(Pa reel destf int flags) {

dest. writeString (name); // 写出 name

dest.writelnt(age); // 写出 age

}

public static final Parcelable.Creator<Person> CREATOR = new Parcelable. Creator<Person>() {

Override

public Person c reateF romPa reel(Pa reel source) {

Person person = new PersonO;

person.name = source.readString(); // 读取name person.age = source.readlnt(); // 读取age return person;

}

Override

public Person[] newArray(int size) { return new Person[size];

}

};

}

        Parcelable的实现方式要稍微复杂一些。可以看到,首先我们让Person类去实现了 Parcelable 接口,这样就必须重写 describeContents()wrlteToParcel()这两个方法。其 中describeContents()方法直接返回0就可以了,而writeToParcel()方法中我们需要调用 ParcelwriteXxx()方法,将Person类中的字段一一写出。注意,字符串型数据就调用 writeStringO方法,整型数据就调用writelnt()方法,以此类推。

        除此之外,我们还必须在Person类中提供一个名为CREATOR的常量,这里创建了 Parcelable.Creator接口的一个实现,并将泛型指定为Person接着需要重写createFrom- Parcel()newArray()这两个方法,在createFromParceK )方法中我们要去读取刚才写出的 nameage字段,并创建一个Person对象进行返回,其中nameage都是调用ParcelreadXxxO方法读取到的,注意这里读取的顺序一定要和刚才写岀的顺序完全相同。而 newArrayO方法中的实现就简单多了,只需要new出一个Person数组,并使用方法中传入的 size作为数组大小就可以了。

        接下来,在FirstActivity中我们仍然可以使用相同的代码来传递Person对象,只不过 在SecondActivity中获取对象的时候需要稍加改动,如下所示:

Person person = (Person) getlntent().getParcelableExtra("persondata");

        注意,这里不再是调用getSerializableExtra()方法,而是调用getParcelableExtra() 方法来获取传递过来的对象了,其他的地方都完全相同。

        这样我们就把使用Intent来传递对象的两种实现方式都学习完了,对比一下,Serializable的 方式较为简单,但由于会把整个对象进行序列化,因此效率会比Parcelable方式低一些,所以在 通常情况下还是更加推荐使用Parcelable的方式来实现Intent传递对象的功能。

13.3定制自己的日志工具

        早在第1章的1.4节中我们就已经学过了 Android日志工具的用法,并且日志工具也确实贯 穿了我们整本书的学习,基本上每一章都有用到过。虽然Android中自带的日志工具功能非常强 大,但也不能说是完全没有缺点,例如在打印日志的控制方面就做得不够好。

        打个比方,你正在编写一个比较庞大的项目,期间为了方便调试,在代码的很多地方都打印 了大量的日志。最近项目已经基本完成了,但是却有一个非常让人头疼的问题,之前用于调试的 那些日志,在项目正式上线之后仍然会照常打印,这样不仅会降低程序的运行效率,还有可能将 一些机密性的数据泄露出去。

        那该怎么办呢?难道要一行一行地把所有打印日志的代码都删掉?显然这不是什么好点子, 不仅费时费力,而且以后你继续维护这个项目的时候可能还会需要这些日志。因此,最理想的情 况是能够自由地控制日志的打印,当程序处于开发阶段时就让日志打印出来,当程序上线了之后 就把日志屏蔽掉。

        看起来好像是挺高级的一个功能,其实并不复杂,我们只需要定制一个自己的日志工具就可 以轻松完成了。比如新建一个LogUtil类,代码如下所示:

public class LogUtil {

public static final int VERBOSE = 1;

public static final int DEBUG = 2;

        可以看到,我们在 LogUtil 中先是定义了 VERBOSE. DEBUG. INFOWARNERROR, NOTHING 6个整型常量,并且它们对应的值都是递增的。然后又定义了一个静态变量level,可以将它 的值指定为上面6个常量中的任意一个。

        接下来我们提供了 v()d()i()w()e()5个自定义的日志方法,在其内部分别调 用了 Log.v()Logd()Log.i()Log.w()Log.e()5个方法来打印日志,只不过在这些 自定义的方法中我们都加入了一个if判断,只有当level的值小于或等于对应日志级别值的时 候,才会将日志打印岀来。

        这样就把一个自定义的日志工具创建好了,之后在项目里我们可以像使用普通的日志工具一 样使用LogUtil,比如打印一行DEBUG级别的日志就可以这样写:

LogUtn.dCTAG", "debug log");

        打印一行WARN级别的日志就可以这样写:

LogUtil.w("TAG”, "warn log");

        然后我们只需要修改level变量的值,就可以自由地控制日志的打印行为了。比如让level 等于VERBOSE就可以把所有的日志都打印出来,让level等于WARN就可以只打印警告以上级 别的日志,让level等于NOTHING就可以把所有日志都屏蔽掉。

        使用了这种方法之后,刚才所说的那个问题就不复存在了,你只需要在开发阶段将level 指定成VERBOSE,当项目正式上线的时候将level指定成NOTHING就可以了。

13.4调试Android程序

        当开发过程中遇到一些奇怪的bug,但又迟迟定位不出来原因是什么的时候,最好的解决办 法就是调试了。调试允许我们逐行地执行代码,并可以实时观察内存中的数据,从而能够比较轻 易地查出问题的原因。那么本节中我们就来学习一下使用Android Studio来调试Android程序的 技巧。

        还记得在第5章的最佳实践环节中编写的那个强制下线程序吗?就让我们通过这个例子来 学习一下Android程序的调试方法吧。这个程序中有一个登录功能,比如说现在登录出现了问题, 我们就可以通过调试来定位问题的原因。

        不用多说,调试工作的第一步肯定是添加断点,这里由于我们要调试登录部分的问题,所以 断点可以加在登录按钮的点击事件里面。添加断点的方法也很简单,只需要在相应代码行的左边 点击一下就可以了,如图13.1所示。

        如果想要取消这个断点,对着它再次点击就可以了。

        添加好了断点,接下来就可以对程序进行调试了,点击Android Studio顶部工具栏中的Debug 按钮(图13.2中最右边的按钮),就会使用调试模式来启动程序。

        等到程序运行起来的时候,首先会看到一个提示框,如图13.3所示

        这个框很快就会自动消失,然后在输入框里输入账号和密码,并点击Login按钮,这时Android Studio就会自动打开Debug窗口,如图13.4所示。

        接下来每按一次F8健,代码就会向下执行一行,并且通过Variables视图还可以看到内存的数据,如图13-5所示。

         可以看到,我们从输入框里获取到的账号密码分别是abc和123,而程序里要求正确的账号 密码是admin123456,所以登录才会出现问题。这样我们就通过调试的方式轻松地把问题定 位出来了,调试完成之后点击Debug窗口中的Stop按钮(图13.6中最下边的按钮)来结束调试 即可。

        这种调试方式虽然完全可以正常工作,但在调试模式下,程序的运行效率将会大大地降低, 如果你的断点加在一个比较靠后的位置,需要执行很多的操作才能运行到这个断点,那么前面这 些操作就都会有一些卡顿的感觉。没关系,Android还提供了另外一种调试的方式,可以让程序 随时进入到调试模式,下面我们就来尝试一下。

        这次不需要选择调试模式来启动程序了,就使用正常的方式来启动程序。由于现在不是在调 试模式下,程序的运行速度比较快,可以先把账号和密码输入好。然后点击Android Studio顶部 工具栏的Attach debugger to Android process按钮13.7中最左边的按钮)。

        此时会弹出一个进程选择提示框,如图13.8所示。

         这里目前只列出了一个进程,也就是我们当前程序的进程。选中这个进程,然后点击0K按 钮,就会让这个进程进入到调试模式了。

        接下来在程序中点击Login按钮,Android Studio同样也会自动打开Debug窗口,之后的流 程就都是相同的了。相比起来,第二种调试方式会比第一种更加灵活,也更加常用。

13.5创建定时任务

        Android中的定时任务一般有两种实现方式,一种是使用Java API里提供的Timer类,一种 是使用AndroidAlarm机制。这两种方式在多数情况下都能实现类似的效果,但Timer有一个 明显的短板,它并不太适用于那些需要长期在后台运行的定时任务。我们都知道,为了能让电池 更加耐用,每种手机都会有自己的休眠策略,Android手机就会在长时间不操作的情况下自动让 CPU进入到睡眠状态,这就有可能导致Timer中的定时任务无法正常运行。而Alarm则具有唤醒 CPU的功能,它可以保证在大多数情况下需要执行定时任务的时候CPU都能正常工作。需要注 意,这里唤醒CPU和唤醒屏幕完全不是一个概念,千万不要产生混淆。

13.5.1 Alarm 机制

        那么首先我们来看一下Alarm机制的用法吧,其实并不复杂,主要就是借助了 AlarmManager 类来实现的。这个类和NotificationManager有点类似,都是通过调用ContextgetSystem ServiceO方法来获取实例的,只是这里需要传入的参数是Context.ALARM. SERVICEO因此, 获取一个AlarmManager的实例就可以写成:

AlarmManager manager = (AlarmManager) getSystemService(Context.ALARMSERVICE);

        接下来调用AlarmManagerset()方法就可以设置一个定时任务了,比如说想要设定一个 任务在10秒钟后执行,就可以写成:

long triggerAtTime = SystemClock.elapsedRealtime() + 10 * 1000;


manager.set(AlarmManager.ELAPSED REALTIME WAKEUP, triggerAtTime, pendingintent);

        上面的两行代码你不一定能看得明白,因为set ()方法中需要传入的3个参数稍微有点复杂, 下面我们就来仔细地分析一下。第一个参数是一个整型参数,用于指定AlarmManager的工作类 型,有4种值可选,分别是ELAPSED_REALTIME.ELAPSED_REALTIME_WAKEUP.RTC RTC_WAKEUPO 其中ELAPSED_REALTIME表示让定时任务的触发时间从系统开机开始算起,但不会唤醒CPUELAPSED_REALTIME_WAKEUP同样表示让定时任务的触发时间从系统开机开始算起,但会唤醒 CPUo RTC表示让定时任务的触发时间从1970110点开始算起,但不会唤醒CPURTC_WAKEUP同样表示让定时任务的触发时间从1970110点开始算起,但会唤醒CPU使用SystemClock.elapsedRealtime()方法可以获取到系统开机至今所经历时间的毫秒数, 使用System. currentTimeMiHis()方法可以获取到1970110点至今所经历时间的 毫秒数。

        然后看一下第二个参数,这个参数就好理解多了,就是定时任务触发的时间,以毫秒为单位。 如果第一个参数使用的是ELAPSED REALTIMEELAPSED_REALTIME_WAKEUP,则这里传入开机 至今的时间再加上延迟执行的时间。如果第一个参数使用的是RTCRTC_WAKEUP,则这里传入 1970110点至今的时间再加上延迟执行的时间。

        第三个参数是一个Pendingintent,对于它你应该已经不会陌生了吧。这里我们一般会调 用getServiceO方法或者getBroadcast()方法来获取一个能够执行服务或广播的Pending- Intent o 这样当定时任务被触发的时候,服务的onStartCommandO方法或广播接收器的 onReceive()方法就可以得到执行。

        了解了 set()方法的每个参数之后,你应该能想到,设定一个任务在10秒钟后执行也可以 写成:

long triggerAtTime = System.currentTimeMillis() + 10 * 1000; 
manager.set(AlarmManager.RTC WAKEUP, triggerAtTime, pendingintent);

        那么,如果我们要实现一个长时间在后台定时运行的服务该怎么做呢?其实很简单,首先新 建一个普通的服务,比如把它起名叫LongRunningService,然后将触发定时任务的代码写到 onStartCommandO方法中,如下所示:

public class LongRunningService extends Service {

^Override

public IBinder onBind(Intent intent) { return null;

}

(QOverride public int onStartCommand(Intent intent, int flags, int startld) { new Th read(new Runnable() {

^Override

public void run() {

//在这里执行具体的逻辑操作

}

}).start();

AlarmManager manager = (AlarmManager) getSystemService(ALARMSERVICE); int anHour = 60 * 60 * 1000; // 这是一小时的毫秒数 -

long triggerAtTime = SystemClock.elapsedRealtime() + anHour;

Intent i = new Intent(thisf LongRunningService.class); Pendingintent pi = Pendingintent.getService(this, 0, i, 0); manager.set(AlarmManager.ELAPSED REALTIME WAKEUP, triggerAtTime, pi); return super.onStartCommand(intent, flags, startld);

} }

        可以看到,我们先是在onStartCommandO方法中开启了一个子线程,这样就可以在这里执 行具体的逻辑操作了。之所以要在于线程里执行逻辑操作,是因为逻辑操作也是需要耗时的,如 果放在主线程里执行可能会对定时任务的准确性造成轻微的影响。

        创建线程之后的代码就是我们刚刚讲解的Alarm机制的用法了,先是获取到了 Ala rm- Manager的实例,然后定义任务的触发时间为一小时后,再使用Pendingintent指定处理定时任务 的服务为LongRurniingService,最后调用set ()方法完成设定。

        这样我们就将一个长时间在后台定时运行的服务成功实现了。因为一旦启动了 LongRunningService,就会在onStartCommand()方法里设定一个定时任务,这样一小时后将会 再次启动LongRunningService,从而也就形成了一个永久的循环,保证LongRunningServiceonSta rtCommand ()方法可以每隔一小时就执行一次。

        最后,只需要在你想要启动定时服务的时候调用如下代码即可:

Intent intent = new Intent(context, LongRunningService.class);

context.startService(intent);

        另外需要注意的是,从Android4.4系统开始,Alarm任务的触发时间将会变得不准确,有可 能会延迟一段时间后任务才能得到执行。这并不是个bug,而是系统在耗电性方面进行的优化。 系统会自动检测目前有多少Alarm任务存在,然后将触发时间相近的几个任务放在一起执行,这 就可以大幅度地减少CPU被唤醒的次数,从而有效延长电池的使用时间。

        当然,如果你要求Alarm任务的执行时间必须准确无误,Android仍然提供了解决方案。 使用AlarmManagersetExact ()方法来替代set ()方法,就基本上可以保证任务能够准时 执行了。

13.5.2 Doze 模式

        虽然Android的每个系统版本都在手机电量方面努力进行优化,不过一直没能解决后台服务 泛滥、手机电量消耗过快的问题。于是在Android 6.0系统中,谷歌加入了一个全新的Doze模式, 从而可以极大幅度地延长电池的使用寿命。本小节中我们就来了解一下这个模式,并且掌握一些 编程时的注意事项。

        首先看一下到底什么是Doze模式。当用户的设备是Android 6.0或以上系统时,如果该设备 未插接电源,处于静止状态(Android 7.0中删除了这一条件),且屏幕关闭了一段时间之后,就 会进入到Doze模式。在Doze模式下,系统会对CPU、网络、Alarm等活动进行限制,从而延 长了电池的使用寿命。

        当然,系统并不会一直处于Doze模式,而是会间歇性地退岀Doze模式一小段时间,在这段 时间中,应用就可以去完成它们的同步操作、Alarm任务,等等。图13.9完整描述了 Doze模式 的工作过程。

         可以看到,随着设备进入Doze模式的时间越长,间歇性地退出Doze模式的时间间隔也会越 长。因为如果设备长时间不使用的话,是没必要频繁退出Doze模式来执行同步等操作的,Android 在这些细节上的把控使得电池寿命进一步得到了延长。

        接下来我们具体看一看在Doze模式下有哪些功能会受到限制吧。

网络访问被禁止。

系统忽略唤醒CPU或者屏幕操作。

系统不再执行WIFI扫描。

系统不再执行同步服务。

□ Alarm任务将会在下次退出Doze模式的时候执行。

        注意其中的最后一条,也就是说,在Doze模式下,我们的Alarm任务将会变得不准时。当 然,这在大多数情况下都是合理的,因为只有当用户长时间不使用手机的时候才会进入Doze模 式,通常在这种情况下对Alarm任务的准时性要求并没有那么高。

        不过,如果你真的有非常特殊的需求,要求Alarm任务即使在Doze模式下也必须正常执行, Android还是提供了解决方案。调用 AlarmManager setAndAllowWhileIdle()setExact- AndAllowWhileIdle()方法就能让定时任务即使在Doze模式下也能正常执行了,这两个方法之 间的区别和set(). setExact()方法之间的区别是一样的。

13.6多窗口模式编程

        由于手机屏幕大小的限制,传统情况下一个手机只能同时打开一个应用程序,无论是 Android. iOS还是Windows Phone都是如此。我们也早就对此习以为常,认为这是理所当然的事 情。而Android 7.0系统中却引入了一个非常有特色的功能——多窗口模式,它允许我们在同一 个屏幕中同时打开两个应用程序。对于手机屏幕越来越大的今天,这个功能确实是越发重要了, 那么本节中我们就将针对这一主题进行学习。

13.6.1进入多窗口模式

        首先你需要知道,我们不用编写任何额外的代码来让应用程序支持多窗口模式。事实上,本 书中所编写的所有项目都是支持多窗口模式的。但是这并不意味着我们就不需要对多窗口模式进 行学习,因为系统化地了解这些知识点才能编写出在多窗口模式下兼容性更好的程序。

        那么先来看一下如何才能进入到多窗口模式。手机的导航栏你肯定是再熟悉不过了,上面一 共有3个按钮,如图13.10所示。

        其中左边的Back按钮和中间的Home按钮我们都经常使用,但是右边的Overview按钮使用 得就比较少了。这个按钮的作用是打开一个最近访问过的活动或任务的列表界面,从而能够方便 地在多个应用程序之间进行切换,如图13.11所示。

        我们可以通过以下两种方式进入多窗口模式。

□在Overview列表界面长按任意一个活动的标题,将该活动拖动到屏幕突出显示的区域, 则可以进入多窗口模式。

□打开任意一个程序,长按Overview按钮,也可以进入多窗口模式。

比如说我们首先打开了 MaterialTest程序,然后长按Overview按钮,效果如图13.12所示。

         可以看到,现在整个屏幕被分成了上下两个部分,MaterialTest程序占据了上半屏,下半屏 仍然还是一个Overview列表界面,另外Overview按钮的样式也有了变化。现在我们可以从 Overview列表中选择任意一个其他程序,比如说这里点击LBSTest,效果如图13.13所示。

        我们还可以将模拟器旋转至水平方向,这样上下分屏的多窗口模式会自动切换成左右分屏的 多窗口模式,如图13.14所示。

         多窗口模式的用法大概就是这个样子了,我们可以将任意两个应用同时打开,这样就能组合 出许多更为丰富的使用场景。比如说刷微博的同时还能时刻关注QQ群消息,看电影的同时还能 和别人一直聊着微信,等等。如果想要退出多窗口模式,只需要再次长按Overview按钮,或者 将屏幕中央的分隔线向屏幕任意一个方向拖动到底即可。

        可以看出,在多窗口模式下,整个应用的界面会缩小很多,那么编写程序时就应该多考虑使 用match parent属性、RecyclerView^ ListViewScrollView等控件,来让应用的界面能够更 好地适配各种不同尺寸的屏幕,尽量不要出现屏幕尺寸变化过大时界面就无法正常显示的情况。

13.6.2多窗口模式下的生命周期

        接下来我们学习一下多窗口模式下的生命周期。其实多窗口模式并不会改变活动原有的生命 周期,只是会将用户最近交互过的那个活动设置为运行状态,而将多窗口模式下另外一个可见的 活动设置为暂停状态。如果这时用户又去和暂停的活动进行交互,那么该活动就变成运行状态, 之前处于运行状态的活动变成暂停状态。

        下面我们还是通过一个例子来更加直观地理解多窗口模式下活动的生命周期。首先打开 MaterialTest项目,修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity {

private static final String TAG = "MaterialTest";

(aOverride

protected void onCreate(Bundle savedlnstanceState) { super.onCreate(savedlnstanceState);

Log.d(TAGr "onCreate'*);

Override protected void onStartO { super.onStartO; Log.d(TAGr "onStart");

}

Override protected void onResumeO { super.onResume(); Log.d(TAGf "onResume");

}

Override protected void onPauseO { super.onPause(); Log.d(TAGr "onPause11);

}

Override protected void onStopO { super.onStop(); Log.d(TAG, ,,onStop,,)j

}

(QOverride

protected void onDestroyO { super.onDestroyO; Log.d(TAG, "onDestroy");

}

@0verride protected void onRestart() { super.onRestart(); Log.d(TAGr "onRestart");

}

        这里我们在Activity7个生命周期回调方法中分别打印了一句日志。

        然后点击Android Studio导航栏上的File—>Open RecenWLBSTest,重新打开LBSTest项目。 修改MainActivity的代码,如下所示:

public class MainActivity extends AppCompatActivity {

private static final String TAG = "LBSTest";

@0verride

protected void onCreate(Bundle savedlnstanceState) { super.onCreate(savedlnstanceState);

Log.d(TAG, "onCreate");

Override protected void onStartO { super.onStartO; Log.d(TAG, "onStarf*)

}

^Override protected void onResume() { super.onResume();

Log.d(TAG, "onResume"); mapView.onResume();

}

^Override

protected void onPause() { super.onPause();

Log.d(TAG, "onPause"); mapView.onPause();

}

Override protected void onStop() { super.onStop(); Log.d(TAG, "onStop**);

}

^Override

protected void onDestroy() { super.onDestroyO; Log.d(TAGr "onDestroy"); mLocationClient.stop(); mapView.onDestroy(); baiduMap.setMyLocationEnabled(false);

}

^Override protected void onRestartO { super.onRestart(); Log.d(TAG, "onRestart'*);

}

}

        这里同样也是在Activity7个生命周期回调方法中分别打印了一句日志。注意这两处日志 的TAG是不一样的,方便我们进行区分。

        现在,先将MaterialTestLBSTest这两个项目的最新代码都运行到模拟器上,然后启动 MaterialTest程序。这时观察logcat中的打印日志(注意要将logcat的过滤器选择为No Filters ), 如图13.15所示。

        可以看到,onCreate()onStartOonResume()方法会依次得到执行,这个也是在我们 意料之中的。然后长按Overview按钮,进入多窗口模式,此时的打印信息如图13.16所示。

         你会发现,MaterialTest中的MainActivity经历了一个重新创建的过程。其实这个是正常现象, 因为进入多窗口模式后活动的大小发生了比较大的变化,此时默认是会重新创建活动的。除此之 外,像横竖屏切换也是会重新创建活动的。进入多窗口模式后,MaterialTest变成了暂停状态。

接着在Overview列表界面选中LBSTest程序,打印信息如图13.17所示。

        可以看到,现在LBSTestonCreate。、onStartOonResume ()方法依次得到了执行, 说明现在LBSTest变成了运行状态。

        接下来我们可以随意操作一下MaterialTest程序,然后观察logcat中的打印日志,如图13.18 所示。

        现在LBSTestonPause()方法得到了执行,而MaterialTestonResume()方法得到了执 行.说明LBSTest变成了暂停状态,MaterialTest则变成了运行状态,这和我们在本小节开头所 分析的生命周期行为是一致的。

        了解了多窗口模式下活动的生命周期规则,那么我们在编写程序的时候,就可以将一些关键 性的点考虑进去了。比如说,在多窗口模式下,用户仍然可以看到处于暂停状态的应用,那么像 视频播放器之类的应用在此时就应该能继续播放视频才对。因此,我们最好不要在活动的 onPauseO方法中去处理视频播放器的暂停逻辑,而是应该在onStopO方法中去处理,并且在 onSta rt ()方法恢复视频的播放。

        另外,针对于进入多窗口模式时活动会被重新创建,如果你想改变这一默认行为,可以在 AndroidManifest.xml中对活动进行如下配置:

<activity

android:name=".MainActivity"

android:label="Fruits"

android: configChanges=l,orientation | keyboardHidden | screensize | screenLayout**>

</activity>

        加入了这行配置之后,不管是进入多窗口模式,还是横竖屏切换,活动都不会被重新创建, 而是会将屏幕发生变化的事件通知到ActivityonConfigurationChanged()方法当中。因此, 如果你想在屏幕发生变化的时候进行相应的逻辑处理,那么在活动中重写onConfiguration- Changed()方法即可。

13.6.3禁用多窗口模式

        多窗口模式虽然功能非常强大,但是未必就适用于所有的程序。比如说,手机游戏就非常不 适合在多窗口模式下运行,很难想象我们如何一边玩着游戏,一边又操作着其他应用。因此, Android还是给我们提供了禁用多窗口模式的选项,如果你非常不希望自己的应用能够在多窗口 模式下运行,那么就可以将这个功能关闭掉。

        禁用多窗口模式的方法非常简单,只需要在AndroidManifest.xml<application><activity>标签中加入如下属性即可:

androidresizeableActivity=["true" | "false"]

        其中,true表示应用支持多窗口模式,false表示应用不支持多窗口模式,如果不配置这 个属性,那么默认值为true

        现在我们将Materialist程序设置为不支持多窗口模式,如下所示:

<application

android:resizeabteActivity="false">

</application>

        可以看到,现在是无法进入到多窗口模式的,而且屏幕下方还会弹出一个Toast提示来告知 用户,当前应用不支持多窗口模式。

        虽说android : resizeableActivity这个属性的用法很简单,但是它还存在着一个问题, 就是这个属性只有当项目的targetSdkVersion指定成24或者更高的时候才会有用,否则这个属性 是无效的。那么比如说我们将项目的targetSdkVersion指定成23,这个时候尝试进入多窗口模式, 结果如图13.20所示。

        可以看到,虽说界面上弹出了一个提示,告知我们此应用在多窗口模式下可能无法正常工作, 但还是进入了多窗口模式。那这样我们就非常头疼了,因为有很多的老项目,它们的 targetSdkVersion都没有指定到24,岂不是这些老项目都无法禁用多窗口模式了?

        针对这种情况,还有一种解决方案o Android规定,如果项目指定的targetSdkVersion低于24, 并且活动是不允许横竖屏切换的,那么该应用也将不支持多窗口模式。

        默认情况下,我们的应用都是可以随着手机的旋转自由地横竖屏切换的,如果想要让应用不 允许横竖屏切换,那么就需要在AndroidManifest.xml<activity>标签中加入如下配置:

android:screenOrientation=["portrait" | "landscape"]

        其中,portrait表示活动只支持竖屏,landscape表示活动只支持横屏。当然android: screenOrientation属性中还有很多其他可选值,不过最常用的就是portraitlandscape 了。

        现在我们将MaterialTestMainActivity设置为只支持竖屏,如下所示:

<activity

android:name=".MainActivity"

android:label="Fruits"

android:screenOrientation="portrait">

</activity>

        重新运行程序之后你会发现MaterialTest现在不支持横竖屏切换了,此时长按Overview按钮 会弹岀和图13.19中一样的提示,说明我们已经成功禁用多窗口模式了。

13.7 Lambda 表达式

        Java 8中着实引入了一些非常有特色的功能,如Lambda表达式、stream API接口默认实现, 等等。虽说我们本地安装的JDK就是Java 8的版本,不过本书中却一直没有使用过任何Java 8的 新特性。这主要是因为我考虑到你对Java 8的新语法规则可能并不熟悉,如果直接应用到项目中的 话,容易让代码难以理解,因此这里我就准备单独使用一节的篇幅来对Java 8的新特性进行讲解。

        虽然刚才已经提到了几个Java8中的新特性,不过现在能够立即应用到项目当中的也就只有 Lambda表达式而已,因为stream API和接口默认实现等特性都只支持Android 7.0及以上的系统, 我们显然不可能为了使用这些新特性而放弃兼容众多低版本的Android手机。而Lambda表达式 却最低兼容到Android2.3系统,基本上可以算是覆盖所有的Android手机了,那么本节中我们就 来重点学习一下Java 8中的Lambda表达式。

        Lambda表达式本质上是一种匿名方法,它既没有方法名,也即没有访问修饰符和返回值类 型,使用它来编写代码将会更加简洁,也更加易读。

        如果想要在Android项目中使用Lambda表达式或者Java 8的其他新特性,首先我们需要在 app/build.gradle中添加如下配置:

android {

defaultConfig {

j ackOptions.enabled = true

}

compileOptions {

sourcecompatibility JavaVersion.VERSI0N_l_8 targetcompatibility JavaVersion.VERSI0N_l_8

} ~ ~

}

        之后就可以开始使用Lambda表达式来编写代码了,比如说传统情况下开启一个子线程的写 法如下:

new Th read(new Runnable() {

(ciOverride

public void run() {

//处理具体的逻辑

}

}).start();

而使用Lambda表达式则可以这样写:

new Thread(() -> {

//处理具体的逻辑

}).start();

        是不是很神奇?不管是从代码行数上还是缩进结构上来看,Lambda表达式的写法明显要更 加精简。

        那么为什么我们可以使用这么神奇的写法呢?这是因为Thread类的构造函数接收的参数是 一个Runnable接口,并且该接口中只有一个待实现方法。我们查看一下Runnable接口的源码, 如下所示:

public interface Runnable {

/**

  • Starts executing the active part of the class' code. This method is
  • called when a thread is started that has been created with a class which
  • implements {code Runnable}.

*/

public void run();

}

        凡是这种只有一个待实现方法的接口,都可以使用Lambda表达式的写法。比如说,通常创 建一个类似于上述接口的匿名类实现需要这样写:

Runnable runnable = new Runnable() {

(QOverride

public void run() {

//添加具体的实现

};

        而有了 Lambda表达式之后我们就可以这样写了:

Runnable runnablel = () -> {

//添加具体的实现

}

        了解了 Lambda表达式的基本写法,接下来我们尝试自定义一个接口,然后再使用Lambda 表达式的方式进行实现。

新建一个MyListener接口,代码如下所示:

public interface MyListener {

String doSomething(String a, int b);

}

        MyListener接口中也只有一个待实现方法,这和Runnable接口的结构是基本一致的。唯一 不同的是,MyListener中的doSomething()方法是有参数并且有返回值的,那么我们就来看一看 这种情况下该如何使用Lambda表达式进行实现。

        其实写法也是比较相似的,使用Lambda表达式创建MyListener接口的匿名实现写法如下:

MyListener listener = (String a, int b) -> {

String result = a + b;

return result;

}

        可以看到,doSomething()方法的参数直接写在括号里面就可以了,而返回值则仍然像往常 一样,写在具体实现的最后一行即可。

        另外,Java还可以根据上下文自动推断出Lambda表达式中的参数类型,因此上面的代码也 可以简化成如下写法:

MyListener listener = (a, b) -> {

String result = a + b;

return result;

}

        Java将会自动推断出参数aString类型,参数bint类型,从而使得我们的代码变得 更加精简了。

        接下来举个具体的例子,比如说现在有一个方法是接收MyListener参数的,如下所示:

public void hello(MyListener listener) {

String a = "Hello Lambda";

int b = 1024;

String result = listener.doSomething(a, b)

Log.d("TAG", result);

        我们在调用heUo()这个方法的时候就可以这样写:

hello((a, b) -> {

String result = a + b;

return result;

})

        那么doSomethingO方法就会将ab两个参数进行相加,从而最终的打印结果就会是 "Hello Lambdal024\

        现在你已经将Lambda表达式的写法基本都掌握了,接下来我们看一看在Android当中有哪 些常用的功能是可以使用Lambda表达式进行替换的。

        其实只要是符合接口中只有一个待实现方法这个规则的功能,都是可以使用Lambda表达式 来编写的。除了刚才举例说明的开启子线程之外,还有像设置点击事件之类的功能也是非常适合 使用Lambda表达式的。

        传统情况下,我们给一个按钮设置点击事件需要这样写:

Button button = (Button) findViewByld(R.id.button);

button.setOnClickListener(new View.OnClickListener() {

^Override

public void onClick(View v) {

//处理点击事件

}

})

        而使用Lambda表达式之后,就可以将代码简化成这个样子了 :

Button button = (Button) findViewByld(R.id.button);

button.setOnClickListener((v) -> {

//处理点击事件

})

        另外,当接口的待实现方法有且只有一个参数的时候,我们还可以进一步简化,将参数外面 的括号去掉,如下所示:

Button button = (Button) findViewByld(R.id.button);

button.setOnClickListener(v -> {

//处理点击事件

})

        这样我们就将Lambda表达式的主要内容都掌握了。当然,有些人可能并不喜欢Lambda表达式这种极简主义的写法。不管你喜欢与否,Java 8对于哪一种写法都是完全支持的,至于到底要不要使用Lambda表达式其实全凭个人,多一种选择总归不是一件坏事情。

13.8总结

        整整13章的内容你已经全部学完了!本书的所有知识点也到此结束,是不是感觉有些激动 呢?下面就让我们来回顾和总结一下这么久以来学过的所有东西吧。

        这13章的内容不算很多,但却已经把Android中绝大部分比较重要的知识点都覆盖到了。 我们从搭建开发环境开始学起,后面逐步学习了四大组件、UI碎片、数据存储、多媒体、网络、 定位服务、Material Design等内容,本章中又学习了如全局获取Context定制日志工具、调试程序、多窗口模式编程、Lambda表达式等高级技巧,相信你已经从一名初学者蜕变成一位Android 开发好手了。

        不过,虽然你已经储备了足够多的知识,并掌握了很多的最佳实践技巧,但是你还从来没有 真正开发过一个完整的项目,也许在将所有学到的知识混合到一起使用的时候,你会感到有些手足无措。因此,前进的脚步仍然不能停下,下一章中我们会结合前面章节所学的内容,一起开发 一个天气预报程序。锻炼的机会可千万不能错过,赶快进入到下一章吧。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/m0_67474913/article/details/128935982

智能推荐

Bean拷贝_java bean拷贝-程序员宅基地

文章浏览阅读1.3k次。因为基础的BeanUtils在使用时拷贝非常不方便,还需要我们自己去创建新的User拷贝,对List集合的拷贝还需要我们自己去遍历,这里我们封装工具类来实现这些功能。VO是后端将前端查询的字段数据封装成VO返给前端,使用Bean拷贝可以实现:将前端查询实体对象转为VO对象。DTO是封装前端传回来的字段,使用Bean拷贝可以实现:将前端传入Dto对象转为实体类对象。注意:两个对象中对应字段名和类型应完全相同,否则无法拷贝。将source中的字段添加到target中。第一个是参数对象,第二个是目标对象。_java bean拷贝

C# Socket发送接收字节数组与十六16进制 转换 函数_c# socket发送hex-程序员宅基地

文章浏览阅读2.5k次。//16进制字符串转字节数组 格式为 string sendMessage = "00 01 00 00 00 06 FF 05 00 64 00 00"; //去空格 保偶数 转化 private static byte[] HexStrTobyte(string hexString) { string ..._c# socket发送hex

一文带你看懂算术编码(C语言)_信息论算术编码c语言实现-程序员宅基地

文章浏览阅读3.9k次,点赞6次,收藏36次。算术编码C语言简介算术编码是图像压缩的主要算法之一。 是一种无损数据压缩方法,也是一种熵编码的方法。和其它熵编码方法不同的地方在于,其他的熵编码方法通常是把输入的消息分割为符号,然后对每个符号进行编码,而算术编码是直接把整个输入的消息编码为一个数,一个满足(0.0 ≤ n < 1.0)的小数n。(百度百科)原理算术编码的基本原理是:根据信源的不同符号序列的概率,把[0,1]区间划分为互不重叠的子区间,子区间的宽度恰好是各符号序列的概率。这样信源发出的不同符号序列将与各子区间一一对应,每个子区间_信息论算术编码c语言实现

【程序员面试金典】04.03. 特定深度节点链表(bfs+链表)_面试题 04.03. 特定深度节点链表 c 语言-程序员宅基地

文章浏览阅读163次。1.题目给定一棵二叉树,设计一个算法,创建含有某一深度上所有节点的链表(比如,若一棵树的深度为 D,则会创建出 D 个链表)。返回一个包含所有深度的链表的数组。示例:输入:[1,2,3,4,5,null,7,8] 1 / \ 2 3 / \ \ 4 5 7 / 8输出:[[1],[2,3..._面试题 04.03. 特定深度节点链表 c 语言

戴尔硬件服务器,服务器硬件、结构介绍_Intel Xeon E5-2660 v4_服务器x86服务器-中关村在线...-程序员宅基地

文章浏览阅读634次。服务器作为一个应用性的重要性远重于外观的IT基础设施,所有服务器的外形几乎都是千篇一律的银色金属制机箱,再加之内部因为搭载同样的x86架构至强系列产品,总让人有一种架构趋同的错觉。但是,服务器的硬件和结构设计却能够体现这个服务器的品质与特点。产品设计是体现产品特点的重要部分,直接关系服务器的性能、可靠性、能耗等指标,比如散热设计和风道设计关系着服务器的散热能力,关系着服务器的稳定性。详细信息大家可..._2660v4

page_to_pfn 、virt_to_page、 virt_to_phys、page、页帧pfn、内核虚拟地址、物理内存地址linux内核源码详解-程序员宅基地

文章浏览阅读5.1k次,点赞24次,收藏72次。page_to_pfn 、virt_to_page、 virt_to_phys、page、页帧pfn、内核虚拟地址、物理内存地址linux内核源码详解_page_to_pfn

随便推点

android 5.0rom官方,一加ROM来了!基于Android 5.0-程序员宅基地

文章浏览阅读719次。每一个手机厂商都会有自家的ROM,就连主打开放的一加也不会例外。除了支持Color OS、MIUI、CM以及YunOS之外,一加已经在此前宣布要开发自家的ROM。2014年最后一天,一加在官方论放出了一加ROM首个体验版。按照官方说法,该ROM是基于AOSP制作的,内核为Android 5.0,最终的目标是做一个与Android L的Material design风格相近的ROM。从网友的反馈来看..._安卓5.0通用刷机包下载

HotFix原理学习 && IL2CPP 学习-程序员宅基地

文章浏览阅读944次,点赞21次,收藏19次。HotFix原理学习 && IL2CPP 学习

Bash中的job管理-程序员宅基地

文章浏览阅读4.3k次。本来不准备写这篇博客的,因为任务管理(job管理)非常非常常用,以至于觉得根本没有必要去写这样一个东西。但想了下,还是记录一下吧,也许有人会用到呢。  不知你是否碰到过这样的情况,当你兴致勃勃的打开VIM,写代码写到正酣时,运营MM或者产品MM气喘吁吁的跑过来:“赶紧帮我跑一下xx的数据,一会做PPT要用”。可是不想直接关闭当前的VIM,而且某些系统下,又不能新开tty(如设置了maxlogi_job管理

python+pytest接口自动化(16)-接口自动化项目中日志的使用 (使用loguru模块)_接口自动化中是不是日志加在用例里-程序员宅基地

文章浏览阅读817次,点赞17次,收藏26次。在自动化测试项目中,一般都需要通过记录日志的方式来确定项目运行的状态及结果,以方便定位问题。_接口自动化中是不是日志加在用例里

@Enable模块装配-程序员宅基地

文章浏览阅读97次。Spring @Enable模块装配定义:具备相同领域功能组件集合,组合所形成一个独立的单元。【简化配置--一起配置】 举例:@EnableWebMvc【自动组装与webMvc相关的东西】、@EnableAutoConfiguration【激活自动装配】等 实现方式:注解方式、编程方式@Enable注解模块举例框架实现 @Enable注解模块 激活模块 Spring Framework @EnableWebMvc WebMvc模块 @EnableTra.

服务端指令-程序员宅基地

文章浏览阅读481次。1: 查看doker日志的存放地点:cd 到dockercompose目录打开docker-compose.yml的文件: vim docker-compose.yml查看容器卷的映射: 前面是相对路径, 后面是docker的路径:./logs:/data/license-management/logs2: 根据ip地址查看日志:cat -n app.log | grep "10.48.21.208"3: 查看实时日志:tail -f app.log结束查..

推荐文章

热门文章

相关标签