使用AsyncTask防止Memory Leaks(内存泄漏)

1. 合理的使用context–比如我们常使用的Toast,Service,最好使用getApplicationContext(),因为这些在activity结束的时候可能仍在运行
下图展示了一些场景我们该用哪种context(图是盗的,附原文地址https://possiblemobile.com/2013/06/context/ 是国外的大牛写的关于context的使用)

继续阅读使用AsyncTask防止Memory Leaks(内存泄漏)

Android获取存储路径

`Android Q(Android 10)`之前,需要添加权限,如下:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

`Android Q(Android 10)`开始 在`App`专属目录下本`App`可以随意操作,无需申请权限,不过`App`专属目录会在`App`卸载时跟随删除。看下面几个目录(通过`Application`的`context`就可以访问)。

  • getFilesDir() :/data/user/0/本应用包名/files
  • getCacheDir():/data/user/0/本应用包名/cache
  • getExternalFilesDir(null):/storage/emulated/0/Android/data/本应用包名/files
  • getExternalCacheDir():/storage/emulated/0/Android/data/本应用包名/cache

`getFilesDir`和`getCacheDir`是在手机自带的一块存储区域(`internal storage`),通常比较小,`SD`卡取出也不会影响到,`App`的`sqlite`数据库和`SharedPreferences`都存储在这里。所以这里应该存放特别私密重要的东西。

`getExternalFilesDir`和`getExternalCacheDir`是在`SD`卡下(`external storage`),在`sdcard/Android/data/包名/files`和`sdcard/Android/data/包名/cache`下,会跟随`App`卸载被删除。

`files`和`cache`下的区别是,在手机设置-找到本应用-在存储中,点击清除缓存,`cache`下的文件会被删除,`files`下的文件不会。

谷歌推荐使用`getExternalFilesDir`。我们项目的下载是个本地功能,下载完成后是存本地数据库的,不是放网络上的,所以下载的音视频都放到了这下面,项目卸载时跟随App都删除了。`getExternalFilesDir`方法需要传入一个参数,传入`null`时得到就是`sdcard/Android/data/包名/files`,传入其他字符串比如"Picture"得到`sdcard/Android/data/包名/files/Picture`。

参考代码如下:

/**
 * 获取app缓存路径
 *
 * @param context Application Context
 * @return 缓存路径
 */
@NonNull
public static String getCacheDir(Context context) {
    String cacheDir;
    if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
            || !Environment.isExternalStorageRemovable()) {
        //外部存储可用
        final File file = context.getExternalCacheDir();
        if (null == file) {
            cacheDir = context.getCacheDir().getPath();
        } else {
            cacheDir = file.getPath();
        }
    } else {
        //外部存储不可用
        cacheDir = context.getCacheDir().getPath();
    }
    return cacheDir;
}

/**
 * 获取app文件路径
 *
 * @param context Application Context
 * @return app文件路径
 */
@NonNull
public static String getFilesDir(Context context) {
    String filesDir;
    if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
            || !Environment.isExternalStorageRemovable()) {
        //外部存储可用
        final File file = context.getExternalFilesDir(null);
        if (null == file) {
            filesDir = context.getFilesDir().getPath();
        } else {
            filesDir = file.getPath();
        }
    } else {
        //外部存储不可用
        filesDir = context.getFilesDir().getPath();
    }
    return filesDir;
}

参考链接

IntelliJ IDEA 提示 'try' can use automatic resource management Java7新特性

`IntelliJ IDEA`会提示

'try' can use automatic resource management。

从` Java 7 build 105`版本开始,`Java 7`的编译器和运行环境支持新的`try-with-resources`语句,称为`ARM` 块(`Automatic Resource Management`) ,自动资源管理。

新的语句支持包括流以及任何可关闭的资源。

public static void filyCopy(File one,File two){
    FileInputStream fileInput = null;
    FileOutputStream fileOutput = null;
    try {
        fileInput = new FileInputStream(one);
        fileOutput = new FileOutputStream(two);
        byte[] b = new byte[1024];
        int len = 0;
        while((len = fileInput.read(b)) != -1){
            fileOutput.write(b, 0, len);
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {//释放资源
        try {
            if(fileInput != null){
                fileInput.close();
            }
            if(fileOutput != null){
                fileOutput.close();
            }
        } catch (Exception e2) {
            e2.printStackTrace();
        }
    }
}

使用`try-with-resources`语句来简化代码如下: 

public static void filyCopy2(File one,File two){
    try (FileInputStream fileInput = new FileInputStream(one);
            FileOutputStream fileOutput = new FileOutputStream(two);){
        byte[] b = new byte[1024];
        int len = 0;
        while((len = fileInput.read(b)) != -1){
            fileOutput.write(b, 0, len);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

在这个例子中,数据流会在`try`执行完毕后自动被关闭,前提是,这些可关闭的资源必须实现`java.lang.AutoCloseable`接口。

注:目前`java.lang.AutoCloseable`接口的子接口或实现类如下:

所有已经子接口: 

AsynchronousByteChannel, AsynchronousChannel, BaseStream, ByteChannel, CachedRowSet, CallableStatement, Channel, Clip, Closeable, Connection, DataLine, DirectoryStream, DoubleStream, FilteredRowSet, GatheringByteChannel, ImageInputStream, ImageOutputStream, InterruptibleChannel, JavaFileManager, JdbcRowSet, JMXConnector, JoinRowSet, Line, LongStream, MidiDevice, MidiDeviceReceiver, MidiDeviceTransmitter, Mixer, MulticastChannel, NetworkChannel, ObjectInput, ObjectOutput, Port, PreparedStatement, ReadableByteChannel, Receiver, RMIConnection, RowSet, ScatteringByteChannel, SecureDirectoryStream, SeekableByteChannel, Sequencer, SourceDataLine, StandardJavaFileManager, Statement, Stream, SyncResolver, Synthesizer, TargetDataLine, Transmitter, WatchService, WebRowSet, WritableByteChannel

 所有已知实现类: 

AbstractInterruptibleChannel, AbstractSelectableChannel, AbstractSelector, AsynchronousFileChannel, AsynchronousServerSocketChannel, AsynchronousSocketChannel, AudioInputStream, BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter, ByteArrayInputStream, ByteArrayOutputStream, CharArrayReader,
CharArrayWriter, CheckedInputStream, CheckedOutputStream, CipherInputStream, CipherOutputStream, DatagramChannel, DatagramSocket, DataInputStream, DataOutputStream,
DeflaterInputStream, DeflaterOutputStream, DigestInputStream, DigestOutputStream, FileCacheImageInputStream, FileCacheImageOutputStream, FileChannel, FileImageInputStream, FileImageOutputStream, FileInputStream, FileLock, FileOutputStream, FileReader, FileSystem, FileWriter, FilterInputStream, FilterOutputStream, FilterReader, FilterWriter, Formatter, ForwardingJavaFileManager, GZIPInputStream, GZIPOutputStream, ImageInputStreamImpl, ImageOutputStreamImpl, InflaterInputStream, InflaterOutputStream,
InputStream, InputStream, InputStream, InputStreamReader, JarFile, JarInputStream, JarOutputStream, LineNumberInputStream, LineNumberReader, LogStream, MemoryCacheImageInputStream, MemoryCacheImageOutputStream, MLet, MulticastSocket, ObjectInputStream, ObjectOutputStream, OutputStream, OutputStream, OutputStream, OutputStreamWriter, Pipe.SinkChannel, Pipe.SourceChannel, PipedInputStream, PipedOutputStream, PipedReader, PipedWriter, PrintStream, PrintWriter, PrivateMLet, ProgressMonitorInputStream, PushbackInputStream, PushbackReader, RandomAccessFile, Reader, RMIConnectionImpl, RMIConnectionImpl_Stub, RMIConnector, RMIIIOPServerImpl, RMIJRMPServerImpl, RMIServerImpl, Scanner, SelectableChannel, Selector, ServerSocket, ServerSocketChannel, Socket, SocketChannel, SSLServerSocket, SSLSocket, StringBufferInputStream, StringReader,StringWriter,URLClassLoader, Writer, XMLDecoder, XMLEncoder, ZipFile,ZipInputStream, ZipOutputStream

对于`Android`用户来说,只有编译工程的 `minSdkVersion`大于` 19`(`Android 4.4`)的时候才能生效。

try-with-resources is only supported if your minSdkVersion is set to 19 or higher.

参考链接


在macOS Catalina(10.15.5)上搭建Flutter开发环境

下载并安装目前最新的`Android Studio 4.0`,然后通过`Android Studio 4.0`安装`Android SDK`。

下载目前最新的`flutter`

$ cd ~

$ mkdir Android

$ cd Android

$ git clone -b stable https://github.com/flutter/flutter.git

配置环境变量

$ export PATH=/Users/`whoami`/Android/flutter/bin:$PATH

# android sdk目录,替换为你自己的即可,下面是Android Studio安装SDK的默认目录 
$ export ANDROID_HOME="/Users/`whoami`/Library/Android/sdk" 
  
$ export PATH=${PATH}:${ANDROID_HOME}/tools
  
$ export PATH=${PATH}:${ANDROID_HOME}/platform-tools
  
$ export PUB_HOSTED_URL=https://pub.flutter-io.cn
  
$ export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

上述环境变量,全部追加到 `.bashrc` 尾部

export PATH=/Users/`whoami`/Android/flutter/bin:$PATH
# android sdk目录,替换为你自己的即可,下面是Android Studio安装SDK的默认目录
export ANDROID_HOME="/Users/`whoami`/Library/Android/sdk"
export PATH=${PATH}:${ANDROID_HOME}/tools
export PATH=${PATH}:${ANDROID_HOME}/platform-tools
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

如果不增加到`.bashrc` 尾部,则在 `Android Studio 4.0` 创建项目的时候,会非常慢,主要是网络问题。

必要的环境配置,依赖下载

#对于 macOS Catalina 10.15.5 来说,由于Shell被替换成了ZSH,因此环境变量需要重新配置一下

$ cp /etc/zprofile ~/.zprofile

$ chmod u+w ~/.zprofile

$ echo "source ~/.bashrc" >> .zprofile

$ flutter doctor

$ flutter doctor --android-licenses

$ sudo gem install cocoapods

# 杀掉全部的dart进程,否则可能导致长时间无法创建项目的问题
# 任何时候,flutter相关卡住,貌似都可以这样操作

$ killall -9 dart

继续阅读在macOS Catalina(10.15.5)上搭建Flutter开发环境

在Android Studio 4.X下查看窗口布局层次Hierarchy Viewer/Layout Inspector

在Android开发的时候,又是需要检查窗口布局层次,观察布局是否显示正确。

早期版本是使用`Hierarchy Viewer` ,最新的Android Studio 4.X下,我们使用 `Layout Inspector`,具体操作参考下图:

继续阅读在Android Studio 4.X下查看窗口布局层次Hierarchy Viewer/Layout Inspector

屏幕旋转的适配问题以及遇到的一些坑

在手机APP开发的时候,一般默认会适配竖屏,游戏开发除外。但是在Android平板电脑开发中,屏幕旋转的问题比较退出,可以这样说,平板电脑的最初用意就是横屏使用的,比较方便,用户会经常旋转我们的屏幕。

这里主要针对平板开发中的一些问题做一些总结。

1. 防止屏幕旋转之后,Activity的销毁问题

为了适配屏幕,Activity默认在屏幕旋转之后会销毁并且重建,但是这种情况会造成用户输入数据的丢失(需要开发者手动去保存和恢复,会带来一定的工作量),Activity毕竟是重量级的组件,它的销毁和重建会使得性能的下降,因此我们需要防止Activity的销毁和重建。

做法

在清单文件中为Activity添加一些配置,configChanges属性添加orientation|screenSize:

<activity
        android:name=".ui.activity.MainActivity"
        android:configChanges="orientation|screenSize"
        android:launchMode="singleTask"
        android:windowSoftInputMode="stateAlwaysHidden">

通过上述的配置可以防止Activity的销毁和重建。

注意:这里的 `screenSize` 经常被我们遗漏。一般我们都是设置 `orientation` , 当屏幕方向改变的时候不要重新创建。那么 `screenSize` 什么时候发生呢? 目前最常见的就是 `系统导航方式` 变化的时候,目前Android手机支持 `手势导航` / `屏幕内三键导航` 两种模式。这两种模式的不同就是在屏幕底部是否出现 `Back`/ `Home` / `Menu`。这样导致应用的显示高度发生变化,从而触发 `screenSize`。

注意,`辅助功能` 里的 `无障碍` 模式下 `导航模式` 会被切换到 `屏幕内三键导航` 模式,从而引起窗口的重新绘制。

2. View屏幕旋转适配

除了制作横屏和竖屏两份布局文件的方法之外,如果我们的View是动态添加到Window的,屏幕旋转之后,我们的界面以及View需要做一些变动以适应屏幕。

我们可以复写Activity的onConfigurationChanged方法,并且在里面修改一些东西。

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

    //一些适配操作
}

这里举个例子,如说我们的ListView右边有一个字母索引控件,这个控件是直接new出来并且是直接覆盖在ListView上面的。当屏幕旋转之后,这个字母索引控件的位置需要刷新(重新布局以及绘制)。因为这个控件是在我们的ListView的右侧,并且竖直方向居中。这时候我们就需要动态获取屏幕上的ListView的宽高,然后才能计算出字母控件应该布局的位置。

但是这里我们会遇到一些坑

直接通过View的getWidth方法或者先measure然后通过getMeasuredWidth方法获取到的宽都是错误的,获取到的是屏幕旋转之前的值,如下面的代码所示。但是我需要的是ListView实时的宽高值,这时候我们只能手动去计算。例如我们要获取ListView的高度,那么我们可以先拿到Window的总高度,然后减去状态栏、Toolbar的高度来获取(这里只是一个例子,具体做法需要具体分析)。

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

    //一些适配操作
    WindowManager wm = getWindowManager();//Activity可以直接获取WindowManager
    //  WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
    final int windowWidth = wm.getDefaultDisplay().getWidth();
    final int windowHeight = wm.getDefaultDisplay().getHeight();

    //手动去计算ListView的宽高
    final int mListViewWidth = windowWidth;
    final int mListViewHeight = windowHeight - statusbarHeight - toolbarHeight;

    //下面获取ListView的宽高是有问题的
    //  mListView.measure(0, 0);
    //  mListView.getMeasuredWidth();
    //  mListView.getMeasuredHeight();

    //  mListView.getWidth();
    //  mListView.getHeight();

    //  。。。其他适配操作
}

屏幕旋转的不确定性问题

最近又遇到屏幕旋转相关的新的问题,因此记录下来。我们知道,在Activity、View、Fragment等旋转的时候,如果你在清单文件中配置不重新创建的话,就会调用onConfigurationChanged方法。

但是问题来了,这个问题有一些不确定性因素,比如说当你的页面或者View被遮挡住(Stop)的时候就不会回调onConfigurationChanged这个方法。这比较略坑,会带来一些UI的偶发性问题。

解决办法就是我们自定义一个广播接受者专门用来接受onConfigurationChanged这个广播,这样子就可以确保,无论什么情况下,系统ConfigurationChanged的时候你的代码都会被执行。

自定义的ConfigurationChangeReceiver如下,这里提供一个静态方法方便注册。

public abstract class ConfigurationChangeReceiver extends BroadcastReceiver {

    private static final String TAG = ConfigurationChangeReceiver.class.getSimpleName();

    public static IntentFilter getIntentFilter() {
    IntentFilter filter = new IntentFilter();
    filter.addAction("android.intent.action.CONFIGURATION_CHANGED");
    return filter;
    }
}

接下类在Activity或者Fragment中的正确位置注册(onAttachedToWindow、onDetachedFromWindow):

private void initConfigurationChangeReceiver() {
    mConfigurationChangeReceiver = new ConfigurationChangeReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            //你的屏幕适配代码
        }
    };
    this.registerReceiver(mConfigurationChangeReceiver, ConfigurationChangeReceiver.getIntentFilter());
}

记得反注册,防止内存泄漏哦。

private void unRegisterConfigurationChangeReceiver() {
    if (mConfigurationChangeReceiver != null) {
        this.unregisterReceiver(mConfigurationChangeReceiver);
    }
}

参考链接


屏幕旋转的适配问题以及遇到的一些坑

Android默认调试签名证书

今天遇到了需要手工用调试签名重新签名APK文件的一个需求。

Android默认调试签名证书位置

Windows:

C:\Users\<用户名>\.android\debug.keystore

macOS Catalina:

~/.android/debug.keystore

证书密码 :android

签名脚本:

$ bash ~/Android/sdk/build-tools/29.0.2/apksigner sign --ks debug.keystore xxx.apk

参考链接


Cannot resolve symbol KeyEventCompat(android.support.v4.view.KeyEventCompat找不到)

今天我把support版本升到了28.0.0  发现V4包  下的KeyEventCompat 类找不到了

com.android.support:appcompat-v4:28.0.0

那是因为KeyEventCompat类被取消了 hasNoModifiers() 方法已经被KeyEvent实现了

修改为:

if (event.hasNoModifiers()) {
      handled = arrowScroll(FOCUS_FORWARD);
} else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
       handled = arrowScroll(FOCUS_BACKWARD);
}

参考链接


Android Q(Android 10)适配

应用读取 Device_ID

Android Q 之前有如下代码,获取设备Id,IMEI等

TelephonyManager telManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
telManager.getDeviceId();
telManager.getImei();

添加下面权限,并且需要动态申请权限

<uses-permission android:name="android.permission.READ_PHONE_STATE" />

在 Android Q 上调用上面方法会报错

java.lang.SecurityException: getDeviceId: The user xxxx does not meet the requirements to access device identifiers.

或者

E/Exception: getSubscriberId: The user xxxx does not meet the requirements to access device identifiers.
    java.lang.SecurityException: getSubscriberId: The user xxxx does not meet the requirements to access device identifiers.

在 Android Q 上上面方法已经不能使用了,如果获取设备唯一Id,需要使用其他方式了,谷歌提供的获取唯一标识符做法见 文档,也可以用Android_ID,上面这些也不是绝对能得到一个永远不变的Id,可能需要多种方案获取其他Id,比如有谷歌商店的手机可以使用谷歌提供的广告Id,还有其他厂商一般都会提供手机的一个唯一Id。

我们项目现在使用下面这种方式 参考链接。要求生成之后,存储在应用的本地数据库中,读取的时候,优先从本地数据库读取上次存储的设备ID,不要每次都调用下面的接口获取,下面的接口不能保证每次都返回相同结果。

public static String getDeviceId(Context context) {
    final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
    if (targetSdkVersion > Build.VERSION_CODES.P && Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
        return getUniqueID(context);
    } else {
        return getTelId(context);
    }
}

private static String getTelId(Context context) {
    final TelephonyManager manager = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
    return manager.getDeviceId();
}

private static String getUniqueID(Context context) {
    String id = null;
    final String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
    if (!TextUtils.isEmpty(androidId) && !"9774d56d682e549c".equals(androidId)) {
        try {
            UUID uuid = UUID.nameUUIDFromBytes(androidId.getBytes("utf8"));
            id = uuid.toString();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    if (TextUtils.isEmpty(id)) {
        id = getUUID();
    }

    return TextUtils.isEmpty(id) ? UUID.randomUUID().toString() : id;
}

private static String getUUID() {
    String serial = null;

    String m_szDevIDShort = "35" +
            Build.BOARD.length() % 10 + Build.BRAND.length() % 10 +

            ((null != Build.CPU_ABI) ? Build.CPU_ABI.length() : 0) % 10 + 
            
            Build.DEVICE.length() % 10 + Build.DISPLAY.length() % 10 + 
            
            Build.HOST.length() % 10 + Build.ID.length() % 10 + 
            
            Build.MANUFACTURER.length() % 10 + Build.MODEL.length() % 10 + 
            
            Build.PRODUCT.length() % 10 + Build.TAGS.length() % 10 + 
            
            Build.TYPE.length() % 10 + Build.USER.length() % 10; //13 位

    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                serial = android.os.Build.getSerial();
            } else {
                serial = Build.SERIAL;
            }
            //API>=9 使用serial号
            return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
        } catch (Exception exception) {
            serial = "serial" + UUID.randomUUID().toString(); // 随便一个初始化
        }
    } else {
        serial = android.os.Build.UNKNOWN + UUID.randomUUID().toString(); // 随便一个初始化
    }

    //使用硬件信息拼凑出来的15位号码
    return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
}

 

参考链接


error=86, Bad CPU type in executable

最近在维护一个`N`久的项目时,发现在`mac`升级系统为`10.15.5`后(Android Studio 4.0,gradle 2.3.1),编译失败了,报错如下:

Cannot run program "/Users/xxxx/Android/sdk/build-tools/23.0.1/aapt": error=86, Bad CPU type in executable

原因是最新版本的`macOS Catalina(10.15.5)`已经不支持`32`位的应用了,只能运行`64`位的应用。

解决方法:升级工程的`buildToolsVersion`,本例中将`23.0.1` 升级成`25.0.3`。

参考链接


error=86, Bad CPU type in executable