透明度 十六进制 % Hex % Hex % Hex % Hex
100% FF 99% FC 98% FA 97% F7 96% F5
95% F2 94% F0 93% ED 92% EB 91% E8
90% E6 89% E3 88% E0 87% DE 86% DB
85% D9 84% D6 83% D4 82% D1 81% CF
80% CC 79% C9 78% C7 77% C4 76% C2
75% BF 74% BD 73% BA 72% B8 71% B5
70% B3 69% B0 68% AD 67% AB 66% A8
65% A6 64% A3 63% A1 62% 9E 61% 9C
60% 99 59% 96 57% 94 56% 91 56% 8F
55% 8C 54% 8A 53% 87 52% 85 51% 82
50% 80 49% 7D 48% 7A 47% 78 46% 75
45% 73 44% 70 43% 6E 42% 6B 41% 69
40% 66 39% 63 38% 61 37% 5E 36% 5C
35% 59 34% 57 33% 54 32% 52 31% 4F
30% 4D 29% 4A 28% 47 27% 45 26% 42
25% 40 24% 3D 23% 3B 22% 38 21% 36
20% 33 19% 30 18% 2E 17% 2B 16% 29
15% 26 14% 24 13% 21 12% 1F 11% 1C
10% 1A 9% 17 8% 14 7% 12 6% 0F
5% 0D 4% 0A 3% 08 2% 05 1% 03

1、删除 .gitignore 排除的文件,例如 build/ .idea/ 等文件

当备份项目源码时,并不需要例如 build 等占用很大磁盘空间的无效文件。当是 git 工程时,可以使用如下命令删除无用文件:

git clean -d -fx [-n(显示删除内容)]

由于 git 工程非常多,写了批量删除的脚本,如下:

#!/bin/bash

CODE_DIR_PATH=''
SHOW_REMOVE_FILE=false
ITERATION_DEGREE=2


while getopts d:nh ops
do
    case ${ops} in
        d)
            CODE_DIR_PATH=${OPTARG}
            ;;
        n)
            SHOW_REMOVE_FILE=true
            ;;
        h)
            echo "help : "
            echo "-d code dir path"
            echo "-n show would remove file"
            echo "-h help"
            exit
            ;;
        *)
            echo "unknow params"
            ;;
    esac

done


if [[ ! -e $CODE_DIR_PATH ]]; then
    echo "error ! resource dir not found"
    exit
fi

array_git_dir=()
index_git_dir=0

function func_search_file()
{
    # echo "func_search_file $1   $2"
    for fileName in ` ls -A $1 `
    do
        if [ -d $1"/"$fileName ] 
        then
            # check git project
            if [[ '.git' = $fileName ]]; then
                # echo $1"/"$fileName
                array_git_dir[index_git_dir++]=$1
                break
            else
                # -ge >=;  -le <=;   -gt >;  -lt <;  -eq ==; -ne !=;
                if [[ $2 -lt $ITERATION_DEGREE && '.' != ${fileName:0:1} ]]; then
                    func_search_file $1"/"$fileName $(($2 + 1))
                fi
            fi
        fi
    done
}

func_search_file $CODE_DIR_PATH 0

# echo ${array_git_dir[@]}

for dir in ${array_git_dir[@]}; do
    echo $dir
    cd $dir
    if [[ 'true' = $SHOW_REMOVE_FILE ]]; then
        git clean -d -fx -n
    else
        git clean -d -fx
    fi
    echo '---done'
done

echo 'end running'  

使用方法如下,其中 「deleteBuild.sh」 为该脚本文件名:

sh deleteBuild.sh -d /Users/ionesmile/Documents/iOnesmileDocs/WorkSpace/

# 使用 -n 查看将要删除的文件,例如
sh deleteBuild.sh -d /Users/ionesmile/Documents/iOnesmileDocs/WorkSpace/ -n

# 使用 -h 帮助
sh deleteBuild.sh -h

编写以上脚本,查询的资料和总结点:

2、一键打包 AAR,并打包运行 Demo

#!/bin/bash

basedir=`cd $(dirname $0); pwd -P`
# echo "START: basedir = $basedir"

GeneProject=/Users/ionesmile/Desktop/Package/FmxosGene
DemoProject=/Users/ionesmile/Desktop/Package/FmxosAAR

versionCode='1'
mVersionName='“1.0.0”'

# 打开 gradle 文件,修改版本配置等
# vim $GeneProject/FmxosPlatform/build.gradle

function logi(){
    echo "#########################################################
    echo "##"
    echo "##   "$*
    echo "##"
    echo "#########################################################
}

function die(){
    echo
    echo "$*"
    echo
    exit 1
}

# 更新版本名称,两个参数。要求辅助输入版本,回车表示不变
# $1:build.gradle 文件路径
# $2:版本号,可选,如果为空时,弹出输入提示
function update_version_name(){
    current_version_name=`grep -o 'versionName *["_a-zA-Z0-9.]*' $1 |sed -n '1p'`
    if [[ -z "$2" ]]; then
        read -p "current $current_version_name, please input: " answer
    else
        answer="$2"
    fi

    if [[ -z "$answer" ]]; then
        echo "answer is empty! continue..."
        # 贪婪匹配最后一个空格并截取
        mVersionName=${current_version_name##* }
    else
        # 版本名,添加双引号
        if [[ ${answer:0:1} != '"' ]]; then
            answer="\"$answer\""
        fi
        mVersionName=$answer
        sed -i '' "s/$current_version_name/versionName $answer/g" $1
        echo "changed: $current_version_name ---> versionName $answer"
    fi
}

function update_version_code(){
    current_version_code=`grep -o 'versionCode *[0-9]*' $1 |sed -n '1p'`
    if [[ -z "$2" ]]; then
        read -p "current $current_version_code, please input: " answer
    else
        answer="$2"
    fi

    if [[ -z "$answer" ]]; then
        echo "answer is empty! continue..."
        # 贪婪匹配最后一个空格并截取
        versionCode=${current_version_code##* }
    else
        versionCode=$answer
        sed -i '' "s/$current_version_code/versionCode $answer/g" $1
        echo "changed: $current_version_code ---> versionCode $answer"
    fi
}

# 更新版本名和版本号,第一个为主版本号,之后为辅助版本号同第一个更新
function update_version(){
    if [[ ! -f $1 ]]; then
        die "GeneProject build.gradle file not exist. -> $1"
    fi
    if [[ ! -f $2 ]]; then
        die "DemoProject build.gradle file not exist. -> $2"
    fi
    update_version_name $1
    update_version_name $2 $mVersionName
    update_version_code $1
    update_version_code $2 $versionCode
}

update_version $GeneProject/FmxosPlatform/build.gradle $DemoProject/app/build.gradle


##########################################################
## 合成代码到单个工程,并生成 AAR,最后打包测试Demo
##########################################################


start_time=$(date +%s)

function gene_aar(){
    # 调用 java,执行合成代码功能
    java -jar $basedir/MergeModule_jar/MergeModule.jar

    # 调用生成 AAR 文件
    cd $GeneProject
    ./gradlew clean
    logi "GeneProject clean done.   "$(($(date +%s)-start_time))"s"
    ./gradlew :FmxosPlatform:assembleRelease
    logi "GeneProject assemble done.   "$(($(date +%s)-start_time))"s"

    # 将 AAR 文件复制到 Demo 中
    cp FmxosPlatform/build/outputs/aar/FmxosPlatform-release.aar $DemoProject/FmxosPlatform/FmxosPlatform-release.aar
    # open FmxosPlatform/build/outputs/aar/
}

function package_demo(){
    # 调用打包测试APK
    cd $DemoProject
    ./gradlew clean
    logi "AARProject clean done.   "$(($(date +%s)-start_time))"s"
    ./gradlew assembleRelease
    logi "AARProject assemble done.   "$(($(date +%s)-start_time))"s"

    # 调用打开 APK 目录
    open app/build/outputs/apk/release/

    # 调用自动安装 apk 到手机
    apkName=$(ls app/build/outputs/apk/release/ | grep '.apk' | sed -n '1p')
    adb install -r app/build/outputs/apk/release/$apkName
    adb shell am start -n com.fmxos.fmxosaar/.MainActivity
}

gene_aar

package_demo

logi 'END... time:'$(($(date +%s)-start_time))"s"

知识点总结:

  1. 获取当前 shell 脚本文件的目录,执行相对路径下的 jar 文件
  2. 使用 grep 命令,查找 versionName,并使用字符串截取值
  3. 使用 sed 命令,修改 versionName 的值
  4. 调用 java -jar 执行 jar 文件,使用 IDEA 导出 jar
  5. 调用 gradlew 打包指定模块的 Release 版本
  6. cp 命令复制文件,open 打卡文件夹
  7. 使用 $(date +%s) 计算命令执行时间差

GitHub: https://github.com/iOnesmile/MergeModuleAAR

近几个月在公司实现了基于喜马拉雅FM音频运营的功能模块,功能比较多,因此细分到不同库去写逻辑。如基础工具库、播放库、文件下载库、网络库等,这样在做其它项目时可以方便移植这些通用功能。为了扩充运营用户量,需要将这一功能模块打包成 SDK,便于分发到其它应用中。经研究使用 AAR 文件的形式,在其它应用集成时最方便。

Android Gradle 打包每个库工程都会导出一个 AAR 文件。之前有尝试使用第三方插件 fat-aar 来合并打包,但打包时经常报错,合并时间也略长。此外此次导出的 SDK 需要做代码混淆,如果对每一个库都进行混淆文件非常麻烦,不便于统一管理,也不便于统一暴露接口。工程库之间的引用逻辑比较多,也增加了导包的配置成本,此外还要支持 AIDL 合并。

最终选择将多个工程库合并到一个工程库后再打包,通过写批处理程序实现。

原本打算使用 Python 开发,但时间较紧张没有太多踩坑机会。由于个人对 Java 的 API 比较熟悉,因此采用了可以完成兼容 Java,且语法更灵活简洁,可以直接贴多行文本避免转义符的 Kotlin 来实现。

代码实现逻辑

一、遍历读取需要合并的文件名

分析需要合并的文件,包含 libs/、src/main/ 和 build.gradle 文件。不需要合并的文件,例如:build/、test/ 等。

方便起见,代码中定义了需要读取文件的目录:

ItemFile("libs"),
ItemFile("src").addItemFile(
        ItemFile("main").addItemFile(
                ItemFile("aidl"),
                ItemFile("assets"),
                ItemFile("java"),
                ItemFile("jniLibs"),
                ItemFile("res"),
                ItemFile("AndroidManifest.xml", false)
        )
),
ItemFile("build.gradle", false)

当然也可以使用类似于 .gitignore 的方式,定义排斥的目录名来实现。

二、写文件

写文件逻辑比较繁琐,需要先读取资源文件和清单文件的内容提取后再合并写出。

  1. aidl/ assets/ java/ 等目录下的文件直接复制,不存在重复不会有冲突
  2. res/values/ 下同名文件(strings、colors、styles等)内容合并
  3. AndroidManifest 合并
    • 合并权限,过滤掉重复权限
    • 合并 标签下内容,如:”meta-data”, “activity”, “service”, “receiver”, “provider”
    • 将以 . 开头的相对 name 替换成绝对路径
      如:android:name=".ui.activity.MusicPlayerActivity" 改成 android:name="com.fmxos.platform.ui.activity.MusicPlayerActivity"
  4. build.gradle 合并
    • 解析 android 节点下配置
    • 解析 dependencies 节点下依赖
    • 去除重复

    用正则表达式匹配,生成结果不是很准确,仅用于最后参考对比。

三、遇到的问题

  1. 统一导包、RBuildConfig

    由于这两个类是自动生成跟随库的包名的,每个库对应一组。合并工程后只剩下一组,之前在代码中引入的这两个文件包名现在无效。

    代码中使用正则表达式进行替换,例如:

    "import com.fmxos.[a-zA-Z_0-9.]*?.R;" to "import com.fmxos.platform.R;"
    

    在 Android Studio 中也可以正则查找替换导包,Shift + Command + R ,选中匹配大小写框和正则框。

四、补充 DataBinding 的替换

在开发应用时为开发方便实用了 databinding 功能,打包后由于要移植到一个老的项目中,而那个项目的 gradle 版本还是 2.10,如果更新到 SDK 实用的 4.1 版本又会带来很多麻烦。不修改 2.10 生成目录是在应用包名下,导致 SDK 中的引用找不到生成后的 DataBinding。自己代码中虽然用到了 databinding,不过只停留在用 findViewbyId() 的功能上,想到这里就决定按照它的格式自己生成一个壳,移除 databinding 来兼容不同版本。

代码实现方式

  1. 通过读取 layout 文件,生成 findViewbyId() 功能的代码
    • layout/ 目录下的文件以 标签开头的就是 databinding 文件
    • 读取文件,匹配包含 id 的标签,并生成代码
  2. 移除文件中的 标签,转移域名到下一级根目录

  3. 替换导包 ViewDataBindingDataBindingUtil

示例代码结构:

创建与 databinding 一样的类名和函数,用于移花接木的壳:

public interface ViewDataBinding {

    View getRoot();
}

public class DataBindingUtil {

    public static <SV extends ViewDataBinding> SV inflate(LayoutInflater layoutInflater, int layoutId, Object o, boolean b) {
        if (layoutId == R.layout.fmxos_activity_base) {
            return (SV) new FmxosActivityBaseBinding(layoutInflater, layoutId);
        }
        return null;
    }
}

自动生成 findViewById 功能的代码,成员函数命名与 databinding 一致:

public class FmxosActivityBaseBinding implements ViewDataBinding {

    private final View mRootView;
    public final android.support.v7.widget.Toolbar toolBar;

    public FmxosActivityBaseBinding(LayoutInflater layoutInflater, int layoutId) {
        mRootView = layoutInflater.inflate(layoutId, null);
        toolBar = (android.support.v7.widget.Toolbar) mRootView.findViewById(R.id.tool_bar);
    }

    @Override
    public View getRoot() {
        return mRootView;
    }
}

项目合成结构

为了方便理解,这里贴出我的项目结构。app 下是引用 SDK 的入口,只包含一个 MainActivity 来调用,右图 FmxosPlatform 工程是合并后的结构。

img_comparison_merge_arr.png

ProGuard 是开源的优化 Java 字节码工具。官方称可用减少 10% 体积,并提升 20% 运行效率。将类名、方法名、变量名混淆成a、b、c基本字母,一定程度上提高了反编译的难度。

  • 压缩(Shrinking):从入口开始建立引用关系网,去除网外为使用的代码。

  • 优化(Optimization):对入口点以外所有的方法进行分析,将其中一部分方法变为 final的,static的,private的或内联的,从而提高执行效率。

  • 混淆(Obfuscation):将入口点以外的类、方法、成员重构为简短的名字,可以减小生成文件体积,同时混淆代码。

官网手册: Proguard Manual Usage

一、Android 中配置

  • 在 build.gradle 中的配置

    buildTypes {
        release {
            minifyEnabled true
            proguardFile getDefaultProguardFile('proguard-android.txt')
            proguardFile 'proguard-rules.pro'
        }
    }
    
    • proguard-android.txt 是官方提供的通用混淆配置,文件路径在 \sdk\tools\proguard\proguard-android.txt
    • proguard-rules.pro 即自定义配置文件

二、混淆规则

  • keep 的使用
    • keep [,modifier,…] class_specification 保留指定的类名以及成员
    • keepclassmembers [,modifier,…] class_specification 留住成员而不能保留住类名
    • keepclasseswithmembers [,modifier,…] class_specification 可以根据成员找到满足条件的所有类而不用指定类名,可以保留类名和成员名
  • modifier 可选值
    • allowshrinking 允许其被压缩,就是说指定的内容有可能被移除,但是如果没有被移除的话它也不会在后续过程中被优化或者混淆.
    • allowoptimization 允许其被优化,但是不会被移除或者混淆(使用情况较少)
    • allowobfuscation 允许其被混淆,但是不会被移除或者优化(使用情况较少)
  • class_specification 规则

    是类和成员的一种模板,只有符合此模板的类和成员才会被应用keep规则。

    • 指定类
      • class 可以指代任意类和接口,interface指明为接口,enum指明为枚举
      • classname 必须用全名,例如 java.lang.String,也可以使用正则表达式(?|\*|\*\*)
      • extendsimplements 关键字是等价的
      • @ 指明类或成员具有某些注解
    • 指定成员
      • \<init\> 代表任意构造方法.
      • \<fields\> 代表任意域.
      • \<methods\> 代表任意方法.
      • * 代表任意成员(包括成员变量和方法).
      • 类型描述通配符
        • % 表示任意基本类型(int,char等,但是不包括void).
        • ? 表示类名中的任意单个字符.
        • * 表示类名中的任意多个字符,不包括分隔符(.).
        • ** 表示类名中的任意多个字符,包括分隔符(.).
        • *** 表示任意类型.
        • ... 表示任意多个任意类型的参数.

三、Android 中不要混淆的类

  • Parcelable 中的 Creator 成员

  • Serializable 的多数子类

    • 并不是所有的子类都不能混淆:只短暂的保持数据,对新版本不会有影响的不需要混淆(如 Bundle 的数据,另外相比于 Parcelable,尽量少用 Serializable)
  • JsonBean 对象
    • 可以使用 Gson 中的 @SerializedName("xxx") 给属性添加注释
  • AIDL 接口的类名

  • 另外通用的(native、R资源、四大组件、view等)

四、Log 日志的优化

  • 当 message 的内容没有计算时(追加、调用方法等),可以直接调用打印
  • 当 message 有计算时,建议使用如下方式

    if (BuildConfig.DEBUG){
        Log.v(TAG, "xxx = " + xxx + "   method = " + method(xxx));
    }
    
    • 当打正式包时 BuildConfig.DEBUG 的值为 false,if 条件中的内容不可能执行,ProGuard 的优化会删除该代码。
    • 如果把这句话封装在方法中,再调用方法。由于在调用方法前先要执行计算操作,虽然不打印但是会多一次无意义的计算

五、使用 @NoProguard 注释

为了方便灵活的配置(类、方法、成员属性)混淆,可以通过定义一个注释标识,让所有引用了该注释的对象都不会混淆。

定义注释:

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
public @interface NotProguard {
}

配置使用了该注释的代码不混淆

# 配置使用了 @NotProguard 注释的类不混淆
-keep @xxx.xxx.NotProguard class * {*;}
# 配置使用了 @NotProguard 注释的成员变量不混淆
-keepclassmembers class * {
@xxx.xxx.NotProguard <fields>;
}
# 配置使用了 @NotProguard 注释的方法不混淆
-keepclassmembers class * {
@xxx.xxx.NotProguard <methods>;
}

六、使用经验补充

使用版本:
Gradle: gradle-4.1-all
android.build: 2.3.2

  • 查看最终的混淆配置文件,如下代码配置输出路径
    -printconfiguration "build/outputs/mapping/configuration.txt"
    
    • 自动生成了 layout 中被引用 View 的构造函数不会混淆的规则
      # view res/layout/fmxos_fragment_pay_track_list.xml #generated:14
      -keep class com.fmxos.platform.ui.view.RichTextView {
          <init>(...);
      }
      

      因此让继承了 View 的类不被混淆的规则是多余的,在代码中被引用的 View 依然可以混淆。

    • 自动生成了使用 @Keep 不被混淆的规则,替代了前面提到的 @NotProguard

      # Understand the @Keep support annotation.
      -keep class android.support.annotation.Keep
      
      -keep @android.support.annotation.Keep class * {
          <fields>;
          <methods>;
      }
      
      -keepclasseswithmembers class * {
          @android.support.annotation.Keep
          <methods>;
      }
      
      -keepclasseswithmembers class * {
          @android.support.annotation.Keep
          <fields>;
      }
      
      -keepclasseswithmembers class * {
          @android.support.annotation.Keep
          <init>(...);
      }
      
    • 自动生成了 AndroidManifest 文件中注册的四大组件类名不混淆规则
      # view AndroidManifest.xml #generated:35
      -keep class com.fmxos.platform.ui.activity.MusicPlayerActivity {
          <init>(...);
      }
      
    • 自动生成了 xml 文件中控件使用了 onclick 属性的方法不混淆规则
      # onClick res/layout/fmxos_patch_music_player_control.xml #generated:8
      -keepclassmembers class * {
          *** onClick(...);
      }
      
    • 其它…

  • Retrofit 的 service 接口定义的参数被 Shrinking,导致参数被删除。可以用 -keepclassmembers 保持这些参数。

主体功能基本完成后开始优化项目。打开【开发者选项】中的【调试 GPU 过渡绘制】后,惊奇的发现自己的应用全部是红色的警告。简单的调试后找到了如下几个原因:

  1. BaseFragment 中设置了背景色,几乎所有的页面都继承了它
  2. ImageView 同时设置了 background 和 src 属性
  3. ListView 的 ItemView 默认情况下有背景色
  4. 打开新的页面时,只使用了 Fragment add(),而没有调用 hide() 隐藏之前的
  5. 给文字下的背景图加的遮罩直接用的一个透明灰色 View,而不是对图片处理
  6. 没有去除 Activity 的默认背景:getWindow().setBackgroundDrawable(null)

处理完这些后页面的警告全部消失。但是处理完第 4 条 hide() 之前的 Fragment 后,又导致如下两个问题:

  1. 开启新的页面,即打开 Fragment 时,当前页面先隐藏,显示空白
  2. 侧滑 Fragment 退出时,上一个 Fragment 还处于隐藏状态,显示空白

这里进入正题,带着疑问研究 Fragment

一、问:调用 add() 方法打开新的 Fragment,为什么绘制层会叠加

答:Fragment 可以简单的理解为 有生命周期的 View,add() 方法是在 ViewGroup 的方法添加了一层 View,这里的重叠就导致了过渡绘制。

处理办法就是调用 hide() 来隐藏旧的 Fragment,下次展示时在调用 show() 方法显示。在源码 FragmentManager 类中可以看到使用 f.mView.setVisibility(View.GONE) 来隐藏 Fragment。

二、问:FragmentTransaction 是什么,设置的动画怎么执行的

答:实现类是 BackStackRecord,管理记录一组操作,有 ArrayList\<Op> 属性。beginTransaction() 方法创建并返回该实例。

回到解决 hide() 导致的问题

  1. 开启新页面时,给当前页面设置 300ms 的延时隐藏动画
  2. 在侧滑触发时,设置上个页面的 Fragment 显示

示例代码如下:

SwipeFragment

public class SwipeBackFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View rootView = inflater.inflate(getLayoutId(), null);
        ...
        return attachSwipe(rootView);
    }

    protected View attachSwipe(View rootView) {
        SwipeBackLayout swipeBackLayout = new SwipeBackLayout(getActivity());
        swipeBackLayout.addView(rootView);
        swipeBackLayout.setContentView(rootView);
        swipeBackLayout.addSwipeListener(new SwipeBackLayout.SwipeListenerEx() {

            private boolean hasSwipeBack = false;
            @Override
            public void onContentViewSwipedBack() {
                if (getActivity() != null) {
                    if (hasSwipeBack) {
                        return;
                    }
                    hasSwipeBack = true;
                    getActivity().getSupportFragmentManager().popBackStack();
                }
            }

            @Override
            public void onScrollStateChange(int state, float scrollPercent) {
            }

            @Override
            public void onEdgeTouch(int edgeFlag) {
                if (getActivity() instanceof XXXActivity) {
                    (((XXXActivity) getActivity()).getFmxosActivityHelper()).showLastFragment();
                }
            }

            @Override
            public void onScrollOverThreshold() {
            }
        });
        return swipeBackLayout;
    }
}

父 Activity

public class FmxosActivity extends FragmentActivity {

    private Stack<Fragment> fragmentStack = new Stack<>();

    @Override
    public void onBackPressed() {
        if (getFragmentManager().getBackStackEntryCount() > 0) {
            getFragmentManager().popBackStack();
            fragmentStack.pop();
            return;
        }
        super.onBackPressed();
    }

    @Override
    public void startFragment(Fragment fragment){
        final int inID = R.anim.fmxos_slide_in_from_right;
        final int outID = R.anim.fmxos_slide_out_to_right;
        FragmentTransaction transaction = getFragmentManager().beginTransaction();
        transaction.setCustomAnimations(inID, R.anim.fmxos_open_fragment_cache, 0, outID)
                .add(R.id.layout_fragment_music_root, fragment)
                .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
                .addToBackStack("fmxosMusic");
        transaction.hide(lastFragment);
        transaction.commitAllowingStateLoss();
        fragmentStack.add(fragment);
    }

    @Override
    public void showLastFragment() {
        Fragment fragment = fragmentStack.get(fragmentStack.size() - 2);
        if (fragment.isHidden()) {
            getFragmentManager().beginTransaction().show(fragment).commitAllowingStateLoss();
        }
    }
}

动画布局:

anim/fmxos_slide_in_from_right
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="300"
    android:fromXDelta="100.0%p"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:toXDelta="0.0" />

anim/fmxos_slide_out_to_right
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="300"
    android:fromXDelta="0.0"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:toXDelta="100.0%p" />

anim/fmxos_open_fragment_cache
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="300"
    android:fromAlpha="100"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:toAlpha="90" />

注:SwipeBackLayout 来源 https://github.com/ikew0ng/SwipeBackLayout

补充

在【调试 GPU 过渡绘制】模式下查看微信,底层的主页面并没有出现红色的过渡渲染。可以猜测背景使用的是上个页面的截图。