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 过渡绘制】模式下查看微信,底层的主页面并没有出现红色的过渡渲染。可以猜测背景使用的是上个页面的截图。

一、声音相关概念

声音是由物体震动产生的,我们可以把从感知的角度分为三种属性:

  • 响度(Loudness),即音量,与振幅有关。
  • 音调(Pitch),即高音和低音,与声音的频率有关系。
  • 音色:使用不同的材质来制作,所表现出来的音色效果是不一样的。

响度和音调只要联想到正弦波非常容易理解,然而音色是什么?

音色 = 基频 + 泛音(多个) 

一个物体发生的同时,会发出很多不同频率的波(谐波)。这许多不同频率的波由于相位差很小(也就是相隔时间很短),人是无法单独分辨的,所以这些波会混合起来一起给人一个整体的感受,而这个感受就叫做音色。

想想就很容易理解了,人的喉咙是立体的,发声时喉咙内每一部分都会产生振动,不同部位产生的振动频率就存在差异。其中频率的相对量最大的决定了声音的音调,其它的频率即泛音。当然人说话时还有鼻子和嘴来协助,另外即便是乐器或其它任何发声物体也往往是整体产生共鸣的结果。

看到一个这样的比喻:如果一个声音中从1到20K赫兹频率的波都有,并且都是1:1的关系,即相对强度都相同。这样一个声音就称为白噪音,听起来就和收音机收不信号时的音色一样。如果我有2万只音箱,每一个音箱分别对应放从1到20k赫兹不同频率的声波。那么我通过开关不同的音箱,调节每个音箱的音量,从理论上讲我就可以得到任何我想要的音色。不论是韩红的声音还是孙楠的声音,小提琴的声音。

声音采集

将模拟信号数字化,分为取样和量化两部分,即通常的 PCM(Pulse-code modulation) 脉冲编码调制技术。

  • 采样速率(Sampling Rate)

    人耳所能辨识的声音范围是 20-20KHZ,根据奈奎斯特抽样定理(要从抽样信号中无失真地恢复原信号,抽样频率应大于 2 倍信号最高频率),所以人们一般都选用 44.1KHZ(CD)、48KHZ 或者96KHZ 来做为采样速率。

  • 采样深度(Bit Depth)

    量化(Quantization) 是将连续值近似为某个范围内有限多个离散值的处理过程,这个范围的宽度离散值的数量表达,会直接影响到音频采样的准确性。一般 8位(256),和 16位(65536)来表示。

  • PCM 文件大小

    存储量 = (采样频率 · 采样位数 · 声道 · 时间)/8 (单位:字节数)
    
    • 采样频率:在16位声卡中有22KHz、44KHz等几级,其中,22KHz相当于普通FM广播的音质,44KHz已相当于CD音质了,目前的常用采样频率都不超过48KHz。
    • 采样位数:在计算机中采样位数一般有8位和16位之分,8位不是说把纵坐标分成8份,而是分成2的8次方即256份; 同理16位是把纵坐标分成2的16次方65536份。
    • 声道数:单声道的声音只能使用一个喇叭发声,立体声的pcm可以使两个喇叭都发声,更能感受到空间效果。
  • 声道和立体声
    • Monaural (单声道)
    • Stereophonic(立体声)
    • 4.1 Surround Sound(4.1环绕立体声)
    • 5.1 Surround Sound(5.1环绕立体声)
  • 音频的几种文件格式
    • 不压缩的格式(UnCompressed Audio Format):PCM数据,wav, aiff
    • 无损压缩格式(Lossless Compressed Audio Format):FLAC, APE, WV, m4a
    • 有损压缩格式(Lossy Compressed Audio Format):mp3, aac

常见的 wav 格式的音频数据其实是 pcm 文件 + 46字节的头信息,头信息记录了 PCM 文件的采样率、采样深度、声道数等信息,可方便播放进行解码。

二、变声原理

变声即是对 PCM 数据进行的处理,如果是其它格式(如:MP3)也需要先解压成 PCM 格式再进行处理。

常用的变声,如女生、男生、小黄人都是对音调(即频率)进行的处理。当音调高时就是女声,低时即男声,常常听到的女声比男声高八度还是有点道理的。

另外还有一些对声音的高级处理,如:混响(Reverb)、回声(Echo)、EQ、锯齿(Flange)等。下面重点说一下混响:

Reverb(或残响)是Reverberation的简写,当一个声音发出后,当它碰到障碍物后会反射,碰到下一个障碍物会再反射,不停反射直至它的能量消失为止。这个持续在空间中反覆反射动作形成的声音集成,就是残响。不是每个频率衰减的速度都一样。同样的声音在同个空间不同位置,到达人耳所经过的反射次数、时间都是不同的,混音时使用 reverb 器材或插件可重新塑造声音的立体空间感,让声音有远近等不同距离的层次。

混音常用的Reverb效果器大概分为两大类。一类是靠电脑程式运算出来的演算式残响(Algorithmic Reverb);另一类是取样式残响(Convolution Reverb)。演算式残响就是利用程式运算,模拟空间的各种反应参数,是人工制造出来的残响。取样式残响是在真实空间中做声音脉冲反应的取样(impulse response),加到欲使用的声音上。

这里区分下 Reverb 和 Echo 的区别:

通常Echo是指声音发出后,要较长时间才会收到反射音的状态,就像我们对着远方的山大喊;「喂~」我们不会马上听到反射回来的声音,通常是喊完后隔了一小段时间才会听到明显反射回来的「喂~喂~~喂~~~」,这种称之为Echo,Echo算是reverb的一种,但 reverb 是个更大的概念。
当回声与原始声音直接的间隔较大时,如 >200ms,我们耳朵能分辨出两个声音的就是 Echo。如果两个声音直接的间隔比较小,通常我们无法分辨出来,与原始声音产生了共鸣的叫 Reverb。

三、第三方处理库

调研中发现的对声音处理的库主要有两个:

  • SoundTouch 是一个开源的音频处理库,用于改变音频流或音频文件的节奏、音调和播放速率。

  • FMOD 声音系统是为游戏开发准备的音频引擎,商业用途需要购买许可证。除了 SoundTouch 只能对声音进行变调处理功能外,还包括了前面提高和没提到的高级功能(Reverb、Echo、EQ、Flange、3D…)。

SoundTouch 与 FMOD 对比

  • SoundTouch

    • 优点:开源!因此具有很高的可塑性,可以自由定制完全适用于自己应用。可以处理音调、速率和节拍功能。

    • 缺点:功能单一,满足不了需求。

    如果只需要处理音调,变男声女声童声等功能使用 SoundTouch 是最佳选择。如果还需要对声音做其它处理,时间充足情况下也可以考虑修改源码,加入相应的算法来达到所需的功能。

  • FMOD

    • 优点:声音处理功能强大,可以方便的对声音进行处理。

    • 缺点:非开源,商用不免费,定制化差。

    虽然目前暂时选择用 FMOD,但是不能快速导出处理后的音频文件依然是硬伤,无法很好的满足产品需求。

FMOD 常见变声和参数说明

  • 萝莉

    提高 8 个音调

  • 大叔

    降低音调到 0.8

  • 惊悚(效果待优化)

    设置颤音效果(Tremolo)

    C++
    system->createDSPByType(FMOD_DSP_TYPE_TREMOLO, &dsp);
    dsp->setParameterFloat(FMOD_DSP_TREMOLO_SKEW, 0.5);
    dsp->setParameterFloat(FMOD_DSP_TREMOLO_FREQUENCY, 20);

  • 搞怪(效果待优化)

    提高语速,x2

  • 空灵(效果待优化)

    设置 Echo

  • 山谷

    设置 Echo

    C++
    system->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
    dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 500);
    dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 22);
    dsp->setParameterFloat(FMOD_DSP_ECHO_WETLEVEL, -15);

  • 礼堂

    设置混响,目前用的 Sfx 的混响模式,参数比较多,在 fmod_common.h 文件的 FMOD_REVERB_PROPERTIES 结构下面列举了值:

    FMOD_PRESET_AUDITORIUM { 4300, 20, 30, 5000, 59, 100, 100, 250, 0, 5850, 64, -11.7f }

  • 教室

    设置混响,Sfx 混响算法

    { 400, 2, 3, 5000, 83, 100, 100, 250, 0, 6050, 88, -9.4f }

  • 现场演出

    设置混响,Sfx 混响算法

    FMOD_PRESET_CONCERTHALL { 3900, 20, 29, 5000, 70, 100, 100, 250, 0, 5650, 80, -9.8f }

  • 机器人(效果待优化)

    设置锯齿(Flange)效果

  • 小黄人(效果待优化)

    提高 8 个音调,加快语速 120%

  • 明亮

    调整 EQ,将 500-2000Hz 的 Q 值调高

四、生活中声音有意思的事(个人理解,科学度待考证)

研究了这么久的声音,回到生活中,解开了一些有趣的小点,原来为什么是这样。

  • 视频的倍速播放范围在 0.5-2 之间

    快速播放时其实是对音频数据的再次采样,并且在数据丢失的同时音调也会发生变化,根据前面采样时提到的奈奎斯特抽样定理,抽样频率应大于 2 倍信号最高频率,否则信号失真而无法完整获取信息,因此因此音频播放过快时而无法得到完整信息。

  • 电话的采样率是 8000Hz(次/每秒)

    人的发声范围为 85HZ~1100HZ,而电话采用 8000Hz 的采样率足以满足语音需求。(发音时还有谐波产生的频率肯定是大于这个范围的,但那个只会影响到音色,对交流没啥影响)

  • 女声比男声高八度

    其实女声比男声只高 4-6 度,并没有所说的 8 度。音乐上男女合唱设计为 8 度是为了能在一个调上(do re mi fa sol la si do)达到和谐的演奏效果。

  • 为嘛需要录音棚

    录音棚除了专业的录音设备,同时可减少噪音录入,混响录音棚的设计更是加强了录音的立体效果。

  • 声纹识别

    所谓声纹(Voiceprint),是用电声学仪器显示的携带言语信息的声波频谱。人在讲话时使用的发声器官–舌、牙齿、喉头、肺、鼻腔在尺寸和形态方面每个人的差异很大,所以任何两个人的声纹图谱都有差异。

五、还有待研究或实现的点

  • 如何准确的变出机器人、小黄人等音效?

    虽然设置了与相关视频中一样的参数,但是仍然无法达到理想的变身效果。可能是因为每个人的音调本身不一样导致,针对个人还需要进行微调等。

    下面是找到的一些变声视频:

  • 如何变某个人的声音,像柯南变声器一样?

    考虑过将自己的声音变成任何人的声音,最开始有一个天真的想法:“先将自己声音的基频提取出来,并分析提取目标声音的音调和泛音等,将自己的基音调至目标音调,并添加目标泛音模型,最后得到目标声音”。不过目前调研这一块比较绝望,还有待今后继续对声音的研究。

    下面是分析提取基频的一些资料:

六、参考资料

一、设置样式

URL:http://www.cnblogs.com/bdsdkrb/p/5715438.html

二、添加滚动

tv_read_text.movementMethod = ScrollingMovementMethod.getInstance()

三、Html.fromHtml() 支持的标签

URL:http://zyoo005.iteye.com/blog/1523874

TextView textView=(TextView)findViewById(R.id.hello);   
textView.setText(Html.fromHtml("Hello <b>World</b>,<font size=\"3\" color=\"red\">AnalysisXmlActivty!</font>"));   
<a href="...">  定义链接内容  
<b>  定义粗体文字   b 是blod的缩写  
<big>  定义大字体的文字  
<blockquote>  引用块标签   
属性:  
Common  -- 一般属性  
cite  -- 被引用内容的URI  
<br>   定义换行  
<cite>   表示引用的URI  
<dfn>   定义标签  dfn 是defining instance的缩写  
<div align="...">  
<em>  强调标签  em 是emphasis的缩写  
<font size="..." color="..." face="...">  
<h1>  
<h2>  
<h3>  
<h4>  
<h5>  
<h6>  
<i>   定义斜体文字  
<img src="...">  
<p>     段落标签,里面可以加入文字,列表,表格等  
<small>  定义小字体的文字  
<strike>   定义删除线样式的文字   不符合标准网页设计的理念,不赞成使用.   strike是strikethrough的缩写  
<strong>   重点强调标签  
<sub>   下标标签   sub 是subscript的缩写  
<sup>   上标标签   sup 是superscript的缩写  
<tt>   定义monospaced字体的文字  不赞成使用.  此标签对中文没意义  tt是teletype or monospaced text style的意思  
<u>   定义带有下划线的文字  u是underlined text style的意思  

一、注意事项

  • 不能热更新 Manifest 清单文件中的内容

二、相关配置

  • enableProxyApplication = true 情况下的配置

    执行 Gradle -> :app -> Tasks -> build -> assemableNone 后,会在 /app/build/bakApk/baseApkDir/none 目录下生成 APK

    检查 APK 的 manifest 文件,ApplicationName 会被替换,并生成几个额外的 meta-data,其中包括 TINKER_ID 和 TINKER_PATCH_APPLICATION

    • TINKER_ID = noneRelease_base-1.54
    • TINKER_PATCH_APPLICATION = xxx.xxx.CustomApplication

三、打包过程

  • 生成基包
    1. 修改 TinkerId 为 base-version
    2. 生成包 Gradle Projects -> :app -> Tasks -> build -> assembleRelease
    3. 获取 APK,在 /app/build/bakApk/baseApkDir/flavors/app-flavor-release.apk
  • 生成补丁包
    1. 修改代码
    2. 修改 TinkerId 为 patch-version-index
    3. 设置 baseApkDir 路径为基包的名称
    4. 生成基包 Gradle Projects -> :app -> Tasks -> tinker-support -> buildAllFlavorsTinkerPatchRelease
    5. 获取 APK,在 /app/build/output/patch/flavors/release/patch_signed_7zip.apk
  • 其它配置
    • buildAllFlavorsDir 构建多渠道补丁时使用
    • isProtectedApp 是否启用加固模式,默认为false
    • enableProxyApplication 是否开启反射Application模式

四、遇到的 BUG

  • 证书
    Execution failed for task ':app:tinkerPatchNoneRelease'.
    > Could not resolve all dependencies for configuration ':app:sevenZipToolsLocator'.
       > Could not download SevenZip-osx-x86_64.exe (com.tencent.mm:SevenZip:1.1.10)
          > Could not get resource 'https://jcenter.bintray.com/com/tencent/mm/SevenZip/1.1.10/SevenZip-1.1.10-osx-x86_64.exe'.
             > Could not GET 'https://jcenter.bintray.com/com/tencent/mm/SevenZip/1.1.10/SevenZip-1.1.10-osx-x86_64.exe'.
                > peer not authenticated
    

    原因:电脑上没有 SevenZip 签名软件

    解决:设置 path 路径

    sevenZip {
        zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
        path = "/usr/local/bin/7za"
    }
    
  • BuglyFileProvider 文件冲突,应用无法安装
    Failed to install xxx.apk: Failure
    [INSTALL_FAILED_CONFLICTING_PROVIDER: Package couldn't be installed in /data/app/com.xxx: 
    Can't install because provider name com.tencent.bugly.beta.fileProvider 
    (in package com.xxx.xxx) is already used by com.xxx.bbb]
    

    原因:android:authorities 不能与已安装应用相同导致。

    在配置清单文件时没有添加 com.tencent.bugly.beta.fileProvider,导致自动生成了一个,并且 android:authorities=”com.tencent.bugly.beta.fileProvider”。

    解决:加上 BuglyFileProvider,代码如下:

    <provider
        android:name="com.tencent.bugly.beta.utils.BuglyFileProvider"
        android:authorities="${applicationId}.fileProvider"
        android:exported="false"
        android:grantUriPermissions="true"
        tools:replace="name,authorities,exported,grantUriPermissions">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/provider_paths"
            tools:replace="name,resource"/>
    </provider>
    

五、使用 Python 编写脚本提取相关信息(不重要)

因为同时要为好多个应用添加 Bugly 的热更新,而且有四个渠道的安装包,手动将每个文件复制出来并重命名非常的繁琐。借此写一个脚本,顺便练练手(之前只看过文档):

import os
import shutil
import time

BASE_PROJECT_BUILD_DIR = "/Users/ionesmile/Documents/iOnesmileDocs/WorkSpace/Snaillove/ColorLampChangda/app/build"

BAK_PATH = BASE_PROJECT_BUILD_DIR + "/bakApk/"

baseApkDir = "app-0206-18-36-11"

FLAVORS_ALIAS_MAP = {"self360": "360", "selfBaidu": "baidu"}


def getApkVersionCode(baseApk):
    return "1.38"


def getProjectName():
    return "i-lamp"


def copyBaseApkRenameToPath(outPath):
    outPath = os.path.join(outPath, "base")
    if not os.path.exists(outPath):
        os.makedirs(outPath)
    for flavorApk in os.listdir(BAK_PATH + baseApkDir):
        if os.path.isdir(os.path.join(BAK_PATH + baseApkDir, flavorApk)):
            baseApk = os.path.join(BAK_PATH + baseApkDir, flavorApk + "/app-"+flavorApk+"-release.apk")
            print "baseApk", baseApk
            if os.path.exists(baseApk):
                if not FLAVORS_ALIAS_MAP.get(flavorApk, None) is None:
                    flavorApk = FLAVORS_ALIAS_MAP.get(flavorApk, None)
                out_file = os.path.join(outPath, "android_"+getProjectName()+"_v"+getApkVersionCode(baseApk)+"_"+getDateYyyyMMdd()+"_"+flavorApk+".apk")
                print "out_file", out_file
                shutil.copyfile(baseApk, out_file)


def copyBaseFileRenameToPath(outPath):
    outPath = os.path.join(outPath, "baseFile")
    if not os.path.exists(outPath):
        os.makedirs(outPath)

    for flavorApk in os.listdir(BAK_PATH + baseApkDir):
        if os.path.isdir(os.path.join(BAK_PATH + baseApkDir, flavorApk)):
            baseApk = os.path.join(BAK_PATH + baseApkDir, flavorApk)

            out_flavor_path = os.path.join(outPath, flavorApk)
            if not os.path.exists(out_flavor_path):
                os.makedirs(out_flavor_path)

            for child_file in os.listdir(baseApk):
                item_file = os.path.join(baseApk, child_file)
                if os.path.isfile(item_file) and not item_file.endswith(".apk"):
                    out_file = os.path.join(out_flavor_path, child_file)
                    print "item_file", item_file
                    print "out_file", out_file
                    print ""
                    shutil.copyfile(item_file, out_file)



def copyPatchApkRenameToPath(outPath):
    outPath = os.path.join(outPath, "patch")
    if not os.path.exists(outPath):
        os.makedirs(outPath)

    patchPath = os.path.join(BASE_PROJECT_BUILD_DIR, "outputs/patch")
    for flavorApk in os.listdir(patchPath):
        if os.path.isdir(os.path.join(patchPath, flavorApk)):
            baseApk = os.path.join(patchPath, flavorApk + "/release/patch_signed_7zip.apk")
            print "baseApk", baseApk
            if os.path.exists(baseApk):
                if not FLAVORS_ALIAS_MAP.get(flavorApk, None) is None:
                    flavorApk = FLAVORS_ALIAS_MAP.get(flavorApk, None)
                out_file = os.path.join(outPath, flavorApk + "_patch_"+getApkVersionCode(baseApk)+"_7zip.apk")
                print "out_file", out_file
                shutil.copyfile(baseApk, out_file)


def getDateYyyyMMdd():
    return time.strftime("%Y%m%d", time.localtime(time.time()))


# 复制基包和 R 等文件到指定的目录
# copyBaseApkRenameToPath("/Users/ionesmile/Desktop/iLamp")
# copyBaseFileRenameToPath("/Users/ionesmile/Desktop/iLamp")

# 提取补丁包到指定目录
copyPatchApkRenameToPath("/Users/ionesmile/Desktop/iLamp")

因为上传不同的市场,需要内置不同的更新包,而 360 和 百度 的更新包在上传时会有冲突,所以需要根据情况来编译对应的更新包。

build.gradle 中 Flavor 编译,并设置输出文件名代码如下:

apply plugin: 'com.android.application'

android {

    productFlavors {
        none { }
        self { }
        self360 { }
        selfBaidu { }
    }

    // 设置输出文件名称(不必须)
    android.applicationVariants.all { variant ->
        variant.outputs.each { output ->
            def flavorAliasMap = ['self360':'360', 'selfBaidu':'baidu']
            String flavorType = flavorAliasMap.get(productFlavors[0].name) == null ? productFlavors[0].name : flavorAliasMap.get(productFlavors[0].name)
            String fileName = "android_ilight_pro_v${defaultConfig.versionName}_${new Date().format("yyyyMMdd")}_${flavorType}.apk"
            output.outputFile = new File(output.outputFile.parent, fileName)
        }
    }
}

dependencies {
    ...

    // 使用 flavorCompile 依赖不同库
    selfCompile(name: 'updateSelfLibrary_20171221', ext: 'aar')
    self360Compile(name: 'update360Library_20171221', ext: 'aar')
    selfBaiduCompile(name: 'updateBaiduLibrary_20171221', ext: 'aar')
}

repositories {
    flatDir { dirs 'libs' }

    jcenter()
}

build.gradle 中签名配置代码如下(不必须):

apply plugin: 'com.android.application'

android {

    // 配置签名包
    signingConfigs {
        config {
            keyAlias 'xxx'
            keyPassword 'xxxxxx'
            storeFile file('/Users/ionesmile/Documents/iOnesmileDocs/WorkDoc/keystore')
            storePassword 'xxxxxx'
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.config
        }
    }
}

目前代码中使用反射的方式来出发检测版本,如下:

/** 动态导入不同的更新库,通过反射方式检查更新 **/
public final static void checkVersionUpdate(Activity activity) {
    final String[] updateClassArr = new String[]{
            "com.snaillove.common.update.DynamicUpdateSelf",
            "com.snaillove.common.update.DynamicUpdateBaidu",
            "com.snaillove.common.update.DynamicUpdate360"
    };
    for (String clazzName : updateClassArr) {
        try {
            Class cls = Class.forName(clazzName);
            Method setMethod = cls.getDeclaredMethod("exec", Activity.class);
            setMethod.invoke(cls.newInstance(), activity);
        } catch (Exception e) {
        }
    }
}

一、接入流程

其实没啥好说的,直接看 快速上手文档SDK API 的使用说明



接入中一些小小的总结:

  1. 下载官网支付 Demo,熟悉调用流程

  2. 按照上面 Training 文档,将 Demo 中的支付相关代码移植到自己的项目中

    • 添加 IInAppBillingService.aidl 文件
    • 设置 com.android.vending.BILLING 权限
    • 调用 IabHelper 对象,完成初始化操作(其中 base64EncodedPublicKey 需要到 控制台 拿取)
  3. 打包当前版本,并上传到控制台
    • 进入控制台: http://play.google.com/apps/publish
    • 在当前应用下【版本管理】 -> 【应用版本】,在 【Aplha 版】 -> 【管理 Alpha 版】 -> 【修改版本】 中上传当前项目打包的安装包
    • 注意接下来测试要和已上传安装包的签名文件、版本号保持一致
    • 在 【设置】 -> 【开发者账号】 -> 【账号详情】 -> 【许可测试】 下添加测试人员账号
  4. 在当前应用下 【商品发布】 -> 【应用内商品】 中添加商品

  5. 查询商品详情 mHelper.queryInventoryAsync

  6. 购买商品 mHelper.launchPurchaseFlow

  7. 消耗商品 mHelper.consumeAsync

二、测试环境

  1. 支持 Google Play 的手机(比如 Nexus,国内很多手机不支持需要 root)
  2. Google 开发者账号和测试账号
  3. 支持双币的信用卡(测试账号付款的时候要用,虽然实际不扣费)
  4. 梯子

三、常见错误

  1. 无法购买您要买的商品

    • 当前Google Play帐号不是测试帐号
    • 当前商品未在后台配置
  2. 此版本的应用为配置为通过Google Play结算。有关详情,请访问帮助中心。
    • 检查下打包所用的签名与上传Google Play后台的签名是否一致
    • 检查版本号与上传的版本号是否一致

前一段时间因为打 AAR 包折腾了一整天,不得不怀疑我对 Gradle 的认识。虽然在此之前确实能解决一些 Gradle 打包依赖的冲突或错误,但并没有系统的去学习。

一、Gradle 是什么

Gradle依赖管理 + 构建工具。它继承了 Ant 的灵活和 Maven 的生命周期管理,它最后被 google 作为了 Android 御用管理工具。它最大的区别是不用 XML 作为配置文件格式,采用了DSL格式,使得脚本更加简洁。

  • Ant 是最早的构建工具,基于 idea,好象是2000年有的,当时是最流行 java 构建工具,不过它的 XML 脚本编写格式让 XML 文件特别大。对工程构建过程中的过程控制特别好。

  • Maven 它是用来给 Ant 补坑的,Maven 第一次支持了从网络上下载的功能,仍然采用 xml 作为配置文件格式,它的问题是不能很好的相同库文件的版本冲突。Maven 专注的是依赖管理,构建神马的并不擅长。

  • 构建工具 是什么

    单个源码文件,你可以很轻松地 javac、gcc。然而项目结构复杂的时候,从源代码到实际产出的生成物之间需要经过一些列的转换操作,比如说编译、打包。而这一整个完整的过剩叫做“构建”。

  • Maven 的主要功能主要分为5点,分别是依赖管理系统、多模块构建、一致的项目结构、一致的构建模型和插件机制。

二、Android 是如何打包的

将一堆源码生成一个 APK 的过程就是打包,Gradle 作为一个构建平台已经有了很好的基础,到具体的打包应用步骤就由 Android Gradle Plugin 完成。即我们在项目下 build.gradle 的配置:

buildscript {
  ...
  dependencies {
    classpath 'com.android.tools.build:gradle:2.3.3'
  }
}

另外在项目子工程中,app、XXXlibrary 内的 build.gradle 文件使用 apply plugin 来指定具体使用的插件,如:

apply plugin: 'com.android.application'
apply plugin: 'com.android.library'

关于该插件打包时更多的配置,请参见(如:buildTypes、productFlavors、signingConfigs、ProGuard):
https://developer.android.com/studio/build/index.html

三、什么是 AAR 文件

AAR 文件本身是一个 zip 文件,在您构建相关应用模块时,库模块将先编译到 AAR 文件中,然后再添加到应用模块中。为了避免常用资源 ID 的资源冲突,请使用在模块(或在所有项目模块)中具有唯一性的前缀或其他一致的命名方案。解压后可以看到如下目录:

  • aapt
  • aidl
  • AndroidManifest.xml
  • assets
  • classes.jar
  • jni
  • libs
  • R.txt
  • res

四、依赖冲突的解决办法

在集成多个库工程时,出现了如下异常:

java.lang.NoSuchMethodError: android.support.v4.app.ActivityCompat.startActivity

很明显是 support 包冲突的问题,查看 gradle.build 文件,在 dependencies 中有如下警告信息:

All com.android.support libraries must use the exact same version specification (mixing versions can lead to runtime crashes).
Found versions 25.2.0, 24.0.0. Examples include com.android.support:animated-vector-drawable:25.2.0 and com.android.support:mediarouter-v7:24.0.0

提示存在多个版本,可能会导致运行时错误,需要使用同一个版本。解决办法:

  • 通过 ./gradlew dependenciesgradle dependencies 查看依赖树

  • 可以使用 exclude 关键字来排除单个的库工程,如:

    compile('com.yanzhenjie:recyclerview-swipe:1.0.3') {
        exclude group: 'com.android.support', module: 'recyclerview-v7'
    }
    
  • 可以使用全局的方式替换,在根目录的 build.gradle 文件中,代码如下:
    subprojects {
        project.configurations.all {
            resolutionStrategy.eachDependency { details ->
                if (details.requested.group == 'com.android.support'
                        && details.requested.name.contains('appcompat-v7') ) {
                    details.useVersion "24.2.0"
                }
            }
        }
    }
    

    并且在项目中可见的地方,修改为相同的版本号,并 sync project gradle

参考:
the-exact-same-version-specification
the-exact-same-version/42582204#42582204

五、Android Studio 如何快速运行程序

运行速度严重影响了开发效率,虽然换 MBP 后比以前的运行速度提高了三倍,但还是不够满意。提高速度主要有如下几个方法:

  1. 提高编译内存,在工程目录下的 gradle.properties 文件中新增如下代码:
    org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
    org.gradle.parallel=true
    org.gradle.daemon=true
    
  2. 在 app 子工程目录下的 build.gradle 中配置下改成增量编译和调整 minSdkVersion
    dexOptions {
        incremental true
    }
    
    productFlavors {
        dev {
            // dev utilizes minSDKVersion = 21
            // to allow the Android gradle plugin
            // to pre-dex each module and produce an APK that can be tested on
            // Android Lollipop without time consuming dex merging processes.
            minSdkVersion 21
        }
        prod {
            // The actual minSdkVersion for the application.
            minSdkVersion 15
        }
    }
    
  3. 使用插件 Freeline

参考文档

上面是我再查找资料中做的一些总结,内容不全面和一些不连贯地方。如要更详细的了解请见如下链接:

由于公司还有一些老项目,不得不再重装一下 Eclipse,中间遇到了不少坑,把流程记录一下。

一、下载安装

  1. 下载 Eclipse

    路径: https://www.eclipse.org/downloads/packages/eclipse-android-developers/neonm6

  2. 下载 SDK

    最开始我下载的是最新的 SDK 版本,但是发现 Android SDK Manager 无法打开。最后尝试使用一个老一点的版本,如:android-sdk_r24.4.1-macosx

  3. 配置 SDK

    在 Eclipse -> Preference -> Android -> SDK Location 中选择下载的版本,并点击 Apply 按钮。

    打开 Android SDK Manager,主要需要下载的有如下几个:

    • Tools
      • Android SDK tools
      • Android SDK Build-tools
    • Android X.X.X (API XX)
      • SDK Platform

二、遇到的异常

  1. 运行时 Unable to build,错误信息:
    Failed to load C:\Program Files (x86)\Android\android-sdk\build-tools\26.0.0-preview\lib\dx.jar   
    Unable to build: the file dx.jar was not loaded from the SDK folder
    

    原因
    build-tools 工具的版本太高出现的问题

    解决
    换一个低版本的 build-tools 工具,如 24.0.0。最简单的方式是在目录下只留下一个,删除其它。

  2. 打签名包时,弹出对话框

    Conversion to Dalvik format failed with error 1
    

    解决

    1. 关闭自动 BuildProject -> Build Automatically
    2. clean 项目 Project -> Clean
    3. 手动 Build,右键点击项目,然后 Build Project
    4. 然后再 Export 项目,报错消失了!

    参考:http://blog.csdn.net/myzlhh/article/details/52279443

  3. 打签名包时,弹出对话框

    No DEX file found.
    

    原因
    eclipse打包的时候,bin路径下的dex文件没有生成,导致无法打包。

    解决

    • 方案一:在打包之前 Clean 并 run 一下项目
    • 方案二:eclipse -> preferences -> android -> build 下,Skip packaging and dexing until export to launch 选项去掉勾选。

    参考:http://blog.csdn.net/henrychow_2015/article/details/60954473