当前位置: 首页 > 工具软件 > Hack4SD > 使用案例 >

读《50 Android Hacks》笔记整理Hack 44~Hack 50

傅安宁
2023-12-01

第十一章 避免代码碎片化

Hack 44 处理熄灯模式

在Android早期,系统在屏幕顶部显示一个状态栏(StatusBar),但在Android的Honeycomb版本上,状态栏移到了屏幕底部。
这里我们在不同的版本中实现全屏效果也就是熄灯模式。
熄灯模式:
在特定情况下,通过减少或隐藏导航栏、动作栏以及系统UI等控件,为用户提供全屏无干扰的视觉体验就是熄灯模式。熄灯模式可以保证用户更好的关注屏幕内容。如果用户需要操作内容,可以通过触摸屏幕等方式退出熄灯模式。

44.1 Android 2.x版本

代码实现如下:

public void onCreate(...){
    ...
    //移除标题栏
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    setContentView(R.layout.main);
    mContentView = findViewById(R.id.content);
    mContentView.setOnClickListener(new OnClickListener(){
        @Override
        public void onClick(View v){
            Window w = getWindow();
            if(mUseFullscreen){
                w.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
                w.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
            }else{
                w.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
                w.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
            }
            mUseFullscreen = !mUseFullscreen;
        }
    });
}

44.2 Android 3.x版本

在这个版本中,标题栏被动作栏(Action Bar)替代,仍然位于屏幕上方,但状态栏被移到屏幕底部。
还有一个重要改变是没有物理按键了,按键都被放置到状态栏中,因此一般只是将其暗化处理。
代码实现如下:

public void onCreate(...){
    ...
    mContentView = findViewById(R.id.content);
    //隐藏或显示动作栏
    mContentView.setOnSystemUiVisibilityChangeListener(new OnSystemUiVisiblilityChange(int visibility){
        ActionBar actionBar = getActionBar();
        if(actionBar != null){
            mContentView.setSystemUiVisibility(visibility);
            //是否可视的参数
            if(visibility == View.STATUS_BAR_VISIBLE){
                actionBar.show();
            }else{
                actionBar.hide();
            }
        }
    });

    mContentView.setOnClickListener(new OnClickListener(){
        @Override
        public void onClick(View v){
            if(mContentView.getSystemUiVisibility() == View.STATUS_BAR_VISIBLE){
                mContentView.setSystemUiVisibility(View.STATUS_BAR_HIDDEN);
            }else{
                mContentView.setSystemUiVisibility(View.STATUS_BAR_VISIBLE);
            }
        }
    });
}

44.3 在一个Activity中整合两种实现

代码实现如下:

Class<?> activity = null;
//坚持Android版本
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB){
    activity = MainActivity2x.class;
}else{
    activity = MainActivity3x.class;
}
//启动不同Activity
startActivity(new Intent(this,activity));
finish();

外链地址1
外链地址2


Hack 45 在旧版本上使用新API

这里我们进行在应用程序中使用Android新API并能运行在老设备上。
演示程序:
第一个API是Android v9中为SharedPreferences.Editor类新添加的apply()方法。
第二个API是在Android API Level 8中引入的,用于在manifest文件中声明是否允许讲应用程序安装在SD卡上。

45.1 使用apply()替代commit()

要操作SharedPreferences类,需要获取一个Editor类,然后调用该类提供的方法来修改SharedPreferences的值。所有相关修改都完成时,需要调用commit()方法。
从Android v9版本开始,SharedPreferences.Editor提供了apply()方法用于替代commit()方法。
官方文档上的解释:
“commit()方法会同步地将偏好值(Preference)直接写入持久化存储设备,与其不同的是,apply()方法会立即把修改内容提交到SharedPreferences内容缓存中,然后开始异步地将修改提交到存储设备上,在这个过程中,开发者不会察觉到任何错误问题。”
所以,如果不需要用到操作的返回值,开发者就应该用apply()方法代替commit()方法。
代码中“…”是那些基础的一般都有的代码。
Activity类中代码如下:

public class MainActivity extends Activity{
    private static final String PREFS_NAME = "main_activity_prefs";
    private static final String TIMES_OPENED_KEY = "times_opened_key";
    private static final String TIMES_OPENED_FMT = "Times opened:%d";

    private TextView mTextView;
    private int mTimesOpened;

    @Override
    public void onCreate(...){
        ...
        mTextView = (TextView) findViewById(R.id.times_opened);
    }

    @Override
    protected void onResume(){
        ...
        SharedPreferences prefs = getSharedPreferences(PREFS_NAME,0);
        mTimesOpened = prefs.getInt(TIMES_OPENED_KEY,1);
        mTextView.setText(String.format(TIMES_OPENED_FMT,mTimesOpened));
    }
    @Override
    protected void onPause(){
        ...
        Editor editor = getSharedPreferences(PREFS_NAME,0).edit();
        editor.putInt(TIMES_OPENED_KEY,mTimesOpened+1);
        SharedPreferencesCompat.apply(editor);
    }
}

SharedPreferencesCompat类代码如下:

public class SharedPreferencesCompat{
    private static final Method sApplyMethod = findApplyMethod();
    private static Method findApplyMethod(){
        //检查apply()方法是否可用
        try{
            Class cls = SharedPreferences.Editor.class;
            return cls.getMethod("apply");
        }catch(NoSuchMethodException unused){}
        return null;
    }
    public static void apply(SharedPreferences.Editor editor){
        if(sApplyMethod != null){
            try{
                //真正调用Editor的apply()方法
                sApplyMethod.invoke(editor);
                return;
                }catch (InvocationTargetException unused) {}catch (IllegalAccessException unused) {}
        }
        editor.commit();//否则调用commit()
    }

}

SharedPreferencesCompat类通过java反射机制检查SharedPreferences.Editor类的apply()方法是否可用。如果可用就将其存放到一个静态变量中。当调用我们创建的apply()方法时,便通过传入的Editor参数真正触发该参数的apply()方法。如果调用失败就调用commit()方法。

45.2 将应用程序安装到SD卡中

从Android v8开始,可以向AndroidManifest中添加一个名为android:installLocation的属性。
该属性开发文档的解释:
“这是一个可选特性,你可以通过在Manifest文件中声明android:installLocation属性来使用这项特性。如果你没有声明,应用程序就会安装到内部存储器上,并且你也不能把它移到外部存储器中。”
要使用上述属性,我们需要在AndroidManifest.xml文件中修改下面几个代码:

//设置android:installLocation属性值为preferExternal
<manifest ...
    package="com.manning.androidhacks.hack045"
    android:versionCode="1"
    android:versionName="1.0"
    android:installLocation="preferExternal">
    //设置minSdkVersion属性值为8
    <uses-sdk android:minSdkVersion="8"/>

我们将android:installLocation属性值设为preferExternal,有SD卡的时候,应用程序就会安装到SD卡上。要使用这个特性,需要将minSdkVersion属性值设置为8。在代码中如果指定上述内容,用户就无法在API 8下级别的Android上安装该应用。(这个现在已经没什么意义了),我们现在开发已经不会兼容8以下的版本了。
这里要注意的一个是:
强行在高版本API Level上编译,会导致类和方法不能向后兼容,如:想在当前运行版本上调用一个该版本中没有提供的方法,会出现java.lang.VerifyError异常。

外链地址1
外链地址2
外链地址3
外链地址4
外链地址5
外链地址6


Hack 46 向后兼容的通知

Android的Jelly Bean版本发布时引入了新的通知(notification)API。通过新API,可以为通知添加动作。通过这些动作,可以在不需要进入应用程序的情况下,对通知做出响应。
要处理通知的所有点击事件,需要使用PendingIntent。PendingIntent与Intent类的最大区别是前者用于延迟执行。
开发文档:
“通过向其他应用程序传送一个PendingIntent,开发者可以为该应用程序赋予执行指定操作的权限,这样其他应用程序就像是你自己的一样(具有相同的权限和身份信息)。PendingIntent本身而言,开发者需要注意构建PendingIntent的方式:通常对于基本的Intent会显示指定某个自己的组件(component),以确保其最终能够发送到指定的目的地,而不会发送到其他地方。”
使用PendingIntent的局限是,开发者无法做出类似“运行着几行代码”这样的操作,只能启动一个Activity、Service或BroadcastReceiver。
在示例程序中,需要涵盖两种类型的操作——一种操作不需要显示UI界面(删除、取消、发送短信),另一种操作需要显示UI界面(读短信、回复短信)。不需要显示UI界面的操作可以用后台逻辑实现,因此我们创建一个名为NotificationHelper的静态类,用于实现所有处理通知的逻辑以及PendingIntent的创建,代码如下:

public class NotificationHelper{
    //由MainActivity调用,以显示通知
    public static void showMsgNotification(Context ctx){
        final NotificationManager mgr;
        mgr = (NotificationManager)ctx.getSystemService(Context.NOTIFICATION_SERVICE);
        NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx).setSmallIcon(android.R.drawable.sym_def_app_icon).setTicker("New msg!").setContentTitle("This is the msg title").setContentText("content...").setContentIntent(getPendingIntent(ctx));
        //添加回复动作
        builder.addAction();
        builder.addAction();
    }
    private static PendingIntent getDeletePendingIntent(Context ctx){
        //与删除短信相关的PendingIntent会用到MsgService
        Intent intent = new Intent(ctx,MsgService.class);
        intent.setAction(MsgService.MSG_DELETE);
        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        return PendingIntent.getService(ctx,0,intent,0);
    }
    private static PendingIntent getReplyPendingIntent(Context ctx){
        //与回复短信相关的PendingIntent会用到ReplyActivity
        Intent intent = new Intent(ctx,ReplyActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        return PendingIntent.getActivity(ctx,0,intent,0);
    }
    private static PendingIntent getPendingIntent(Context ctx){
        //点击通知时,会通过MsgActivity显示短信
        Intent intent = new Intent(ctx,MsgActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        return PendingIntent.getActivity(ctx,0,intent,0);
    }
    public static void dismissMsgNotification(Context ctx){
        //取消通知的辅助方法
        final NotificationManager mgr;
        mgr = (NotificationManager)ctx.getSystemService(Context.NOTIFICATION_SERVICE);
        mgr.cancel(R.id.activity_main_receive_msg);
    }
}

通过NotificationHelper类,可以处理所有与通知相关的操作。现在,我们来分析MsgService的一部分代码。因为MsgService继承自IntentService,其onHandleIntent()方法的代码如下:

@Override
protected void onHandleIntent(Intent intent){
    if(MSG_RECEIVE.equals(intent.getAction())){
        handleMsgReceive();
    }else if(MSG_DELETE.equals(intent.getAction())){
        handleMsgDelete();
    }else if(MSG_REPLY.equals(intent.getAction())){
        handleMsgReply(intent.getStringExtra(MSG_REPLY_KEY));
    }
}

handleMsgDelete()方法代码分析如下:

private void handleMsgDelete(){
    //这里是移除短信而不是创建日志
    Log.d(TAG,"Removing msg...");
    //关闭通知
    NotificationHelper.dismissMsgNotification(this);
}

当点击MsgActivity的删除按钮时,我们让MsgService全权处理。如MsgActivity中的删除按钮的点击事件处理器:

public void onDeleteClick(View v){
    Intent intent = new Intent(this,MsgService.class);
    intent.setAction(MsgService.MSG_DELETE);
    startService(intent);
    finish();
}

外链地址1
外链地址2
外链地址3


Hack 47 使用Fragment创建Tab

我们在之前开发会用到TabActivity类,该类允许开发者在应用程序中创建Tab,这样用户就可以通过点击Tab按钮在不同Activity之间切换。TabActivity类最大的问题是在定制其外观时,会遇到很多问题,而且该类在Fragment发布后已经废弃。
尽管Android SDK提供了TabHost和TabWidget等类用于处理Tab,但还是没有使用自己实现的更灵活。

47.1 创建自定义Tab的UI界面

首先,我们需要为Tab创建UI界面。
代码如下:

<LinearLayout ...
    android:background="@null"
    android:orientation="horizontal">
    <Button
        android:id="@+id/tab_red"
        .../>
    <Button
        android:id="@+id/tab_green"
        .../>
    <Button
        android:id="@+id/tab_blue"
        .../>
</LinearLayout>

47.2 在Activity中放置Tab

为了避免每个Activity中都复制/粘贴Tab的布局文件,所以我们使用include标签。MainActivity的XML布局代码如下:

<FrameLayout ...>
    <!-- Fragment的容器 -->
    <FrameLayout 
        android:id="@+id/main_fragment_container"
        .../>
    <!-- 将Tab的布局文件添加到Activity的视图中 -->
    <include layout="@layout/tabs"/>
</FrameLayout>

我们将include标签放到底部是为了让其显示在Fragment容器上方。
至此,UI界面已经完成,下面是Activity的处理逻辑,代码如下:

//继承FragmentActivity
public class MainActivity extends FragmentActivity{
    @Override
    public void onCreate(...){
        ...
        setContentView(R.layout.main);

        //为按钮设置点击监听器,进而通过新建一个Fragment实例调用switchFragment()方法
        findViewById(R.id.tab_red).setOnClickListener(new OnClickListener(){
                @Override
                public void onClick(View v){
                    switchFragment(ColorFragment.newInstance(Color.RED,"Red"));
                }
            });
            ...
    }
    //处理在容器内显示Fragment的逻辑
    private void switchFragment(Fragment fragment){
        FragmentTransaction ft;
        ft = getSupportFragmentManager().beginTransaction();
        ft.replace(R.id.main_fragment_container,fragment);
        ft.commit();
    }
}

外链地址1
外链地址2


第十二章 构建工具

Hack 48 使用Apache Maven处理依赖关系

开发者可以使用Apache Maven处理依赖关系。
要理解Maven的工作原理,我们需要分析pom.xml文件的各个不同片段。pom.xml文件是需要在项目中配置的唯一一个与Maven相关的文件。在该文件中,我们需要向Maven提供应用程序的名称、构建时所需的依赖关系、测试相关的依赖关系以及如何创建APK。Maven首先检查这些依赖关系是否存在于本地仓库(local repository)。默认情况下,本地仓库的路径是~/.m2/repository。如果在本地仓库中没有找到指定的依赖关系,Marven会从中心仓库(central repository,是Maven默认的远程仓库)中下载依赖关系。
pom.xml文件中第一段代码如下:

<project xmlns="http://Maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://Maven.apache.org/POM/4.0.0http://Maven.apache.org/Maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.roboguice</groupId>
    <artifactId>calculator</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>apk</packaging>
    <name>calculator</name>

最终更多构建结果会存在于$MVN_REPO/groupId/artifactId/version中。通常情况下,示例程序会使用groupId作为项目名,使用artifactId作为模块名。在本例中,Manfred之所以使用org.roboguice作为项目名,是因为本例是基于roboguice(一种依赖注入库)项目开发的。在该项目中,artifactId和calculator用于标识当前示例程序。
最后两个属性是packaging和name。packaging为Maven指定最终输出文件的类型,默认值为jar。因为我们需要创建一个Android程序,所以Manfred将该值指定为apk。name和version共同决定输出文件的名字。
pom.xml第二部分的部分依赖关系代码如下:

<dependencies>
    <dependency>
        <groupId>org.roboguice</groupId>
        <!-- roboguice依赖 -->
        <artifactId>roboguice</artifactId>
        <version>2.0-SNAPSHOT</version>
    </dependency>
    ...
    <dependency>
        <groupId>com.google.android</groupId>
        <!-- Android依赖 -->
        <artifactId>android</artifactId>
        <version>2.3.3</version>
        <scope>provided</scope>
    </dependency>
    ...
    <dependency>
        <groupId>com.pivotallabs</groupId>
        <!-- robolectric依赖 -->
        <artifactId>robolectric</artifactId>
        <version>1.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

每个依赖关系都有四个重要属性,即groupId、artifactId、version和scope。第一个依赖关系是roboguice,定义了groupId、artifactId、version属性,用于关联某个Maven仓库中依赖关系的一个发布版本。
尽管roboguice依赖关系中没有包含scope属性,但我们应该知道其实使用了默认值compile。因为编译范围的依赖关系(Compile Dependencies)会被打包到APK中,因此这种依赖关系在项目所有类路径(classpath)中可用。
下一个依赖关系是Android本身。使用Maven编译Android应用程序时,必须将Android视为一种依赖关系,这种依赖关系的scope属性值为provided。provided与compile有很多相似之处,但provided表示希望在运行时由JDK或容器提供依赖关系。本例由运行Android的设备提供依赖关系。
最后一种依赖关系是robolectric。robolectric是一个测试框架,因此我们只在编译/运行测试代码的时候需要这种依赖关系。这就是将scope设置为test的目,在正常使用应用程序时,不需要这种测试范围的依赖关系,只有在编译或执行测试代码的阶段才会用到这种依赖关系。
pom.xml文件中,配置build部分代码如下:

<build>
    <plugins>
        <plugin>
            <groupId>
                com.jayway.Maven.plugins.android.generation2
            </groupId>
            <artifactId>
                android-Maven-plugin
            </artifactId>
            <version>3.0.0-SNAPSHOT</version>
            <configuration>
                <androidManifestFile>
                    ${project.basedir}/AndroidManifest.xml
                </androidManifestFile>
                <assetsDirectory>
                    ${project.basedir}/assets
                </assetsDirectory>
                <resourceDirectory>
                    ${project.basedir}/res
                </resourceDirectory>

                <sdk>
                    <platform>10</platform>
                </sdk>
                <undeployBeforeDeploy>true</undeployBeforeDeploy>
            </configuration>
            <extensions>true</extensions>
            ...
        </plugin>
    </plugins>
</build>

构建插件与依赖关系的工作原理相似。上面代码表示android-Maven-plugin插件是如何配置的。如果配置依赖关系,则需要提供groupId、artifactId和version属性。
我们可以注意到,Apache Maven遵循“约定优于配置”(convention-over-configuration)的规范,这样做的原因是减少软件开发人员所做决定的数量,应用简单而又不失灵活性。
pom.xml文件准备就绪后,开发者就可以把Android应用程序作为一个Maven构件。如果运行mvn package命令,会生成一个存放APK的目标路径。如果想在连接的设备上安装应用程序,可以运行mvn android:deploy命令。
如果理解了该工具的运行原理,只需生成pom.xml文件就可以创建一个项目。
外链地址1
外链地址2
外链地址3
外链地址4
外链地址5
外链地址6
外链地址7


Hack 49 在root过的设备上安装依赖库

在将应用程序安装到设备之前,与java虚拟机兼容的.class字节码文件会被转换为与Dalvik虚拟机兼容的.dex文件。
Android支持添加JAR包作为依赖库,但每次构建代码时,都需要先将JAR包中的.class文件转化为.dex文件,这样消耗了大量的时间。
这里我们演示如何在开发设备上安装这些依赖库,以避过依赖库的dex转换阶段,节省构建时间。
我们这里只是把依赖库安装在已经root过的设备上,这意味着这种方式无法应用于最终产品上,所以使用该方法的目的是为了节省开发阶段构建应用程序的时间。

49.1 dex预处理

第一步是对依赖库进行dex预处理,就是首先将JAR文件转化为Dex文件。可以通过ANDROID_SDK/tools目录下提供的dx工具完成上述操作。
示例:如果依赖库是dep.jar,我们需要运行以下命令

dx -JXmx1024M -JXms1024M -JXss4M
    --no-optimize --debug --dex
    --output=./dep_dex.jar dep.jar

我们会把生成的dep_dex.jar文件上传到设备中。

49.2 创建与权限相关的XML文件

第二步是创建XML文件,为每个依赖库指定权限,代码如下:

<permissions>
    <!-- 这里指定库命,指定需要dex预处理的文件路径 -->
    <library name="dep" file="/data/data/com.dep.package/files/dep_dex.jar"/>
</permissions>

首先需要指定库命,该库命便是use-library标签中使用的库名。此外,还需要指定已经经过dex预处理的文件在设备上的存储路径。可以使用adb命令或通过一个android应用把经过dex预处理的文件上传到设备上。

49.3 修改AndroidManifest.xml文件

最后一步是修改AndridManifest.xml文件,以便使用安装在设备上的依赖库。使用上面的dep库方法如下:

<uses-library name="dep"/>

还有要记得修改构建工具的配置,以避免编译这些依赖库。如:在Apache Maven中,可以将scope属性设置为provided。

这个方法的缺点:
1.需要一个root过的设备,但很遗憾并不是所有Android设备都经过root。
2.需要修改构建脚本以避免运行于目标产品上。
在这种情况下,Apache Maven可以作为处理不同类型构建过程的好工具。
外链地址1
外链地址2
外链地址3


Hack 50 使用Jenkins处理设备多样性

在这里我们会用到一款名为Jenkins的软件——一种流行的开源可持续化集成服务器,并附带一个Android模拟器插件。

50.1 创建Jenkins job

在Jenkins中,点击New Job后输入Job名称,选择Build Multiconfiguration Project(也称为Matrix Job)并点击OK按钮。
在Job配置信息中,首先要输入源码管理信息以便Jenkins可以签出(check out)应用程序并测试代码库。
为了Jenkins能够监控代码库的变化,需要开启周期性构建选项,输入cron风格的语句。
接下来,在构建过程中,点击“Run an Android Emulator during build”,并在”Run Emulator with Properties”中输入以下值:
1.Android操作系统版本:${os}
2.屏幕密度:240
3.屏幕分辨率:WVGA
都配置完成后点击Save按钮结束Job的配置过程。这时我们就拥有了一个Jenkins job。

50.2 运行job

在job页面的左侧面板点击Build Now,几秒后,就会看到一些球开始闪烁,这预示着一些“Configurations”正在构建。
同时,我们也可以通过点击某个闪烁的球,然后点击左侧面板的蓝色进度条来观察构建过程。
当Jenkins的侧边栏中的进度条消失时,构建过程就完成了。
外链地址1
外链地址2
外链地址3
外链地址4
外链地址5

 类似资料: