0%

昨天按照github上Xposed的官方教程写Xposed模块,但是怎么也运行不起来。后来查log发现报错了。错误是 java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation

解决方法是:鼠标右键点击工程,选择“Open Module Settings”,在弹出的窗口中打开Dependencies选项卡。把XposedBridgeApi这个jar包后面的Scope属性改成provided。

解决方案来源。有一篇文章讲了原理,以后留着看。

这是Xposed在github上的最后一篇教程。原文地址

Xposed使得替换资源,比如:图像和字符串之类的事变得容易。下面是做法:

# 简单的资源

@Override
public void initZygote(IXposedHookZygoteInit.StartupParam startupParam) throws Throwable {
    XResources.setSystemWideReplacement("android", "bool", "config_unplugTurnsOnScreen", false);
}

@Override
public void handleInitPackageResources(InitPackageResourcesParam resparam) throws Throwable {
    // replacements only for SystemUI
    if (!resparam.packageName.equals("com.android.systemui"))
        return;

    // different ways to specify the resources to be replaced
    resparam.res.setReplacement(0x7f080083, "YEAH!"); // WLAN toggle text. You should not do this because the id is not fixed. Only for framework resources, you could use android.R.string.something
    resparam.res.setReplacement("com.android.systemui:string/quickpanel_bluetooth_text", "WOO!");
    resparam.res.setReplacement("com.android.systemui", "string", "quickpanel_gps_text", "HOO!");
    resparam.res.setReplacement("com.android.systemui", "integer", "config_maxLevelOfSignalStrengthIndicator", 6);
}

这就是“简单”的替换,通过这种方式你可以直接替换值。这种方式可以用于:Boolean, Color, Integer, int[], String and String[]。

如你所见,有几种不同的设置替换资源的方式。对于构成安卓框架一部分的所有地方都应该被替换的资源(对所有的app都可用),你应在initZygote中调用XResources.setSystemWideReplacement(...)方法。对于特定app的资源,你需要核实过你确实在正确的app之后在hookInitPackageResources中调用res.setReplacement。这时你不应使用setSystemWideReplacement因为可能会产生你无法预料的副作用。

替换Drawable也采用相似的办法。然而你不能只使用Drawable作为替换物,因为这可能导致同一个Drawable实例被不同的ImageViews引用。因此,你需要使用包装器:

resparam.res.setReplacement("com.android.systemui", "drawable", "status_bar_background", new XResources.DrawableLoader() {
    @Override
    public Drawable newDrawable(XResources res, int id) throws Throwable {
        return new ColorDrawable(Color.WHITE);
    }
});

# 复杂资源

更加复杂的资源(比如动画类型的Drawable)必须从你的模块资源当中被引用。我们假设你想要替换电池图标。下面是代码:

package de.robv.android.xposed.mods.coloredcirclebattery;

import android.content.res.XModuleResources;
import de.robv.android.xposed.IXposedHookInitPackageResources;
import de.robv.android.xposed.IXposedHookZygoteInit;
import de.robv.android.xposed.callbacks.XC_InitPackageResources.InitPackageResourcesParam;

public class ColoredCircleBattery implements IXposedHookZygoteInit, IXposedHookInitPackageResources {
    private static String MODULE_PATH = null;

    @Override
    public void initZygote(StartupParam startupParam) throws Throwable {
        MODULE_PATH = startupParam.modulePath;
    }

    @Override
    public void handleInitPackageResources(InitPackageResourcesParam resparam) throws Throwable {
        if (!resparam.packageName.equals("com.android.systemui"))
            return;

        XModuleResources modRes = XModuleResources.createInstance(MODULE_PATH, resparam.res);
        resparam.res.setReplacement("com.android.systemui", "drawable", "stat_sys_battery", modRes.fwd(R.drawable.battery_icon));
        resparam.res.setReplacement("com.android.systemui", "drawable", "stat_sys_battery_charge", modRes.fwd(R.drawable.battery_icon_charge));
    }
}

你可以随意命名你的替换资源。我选择 batterry_icon 替代 stat_sys_battery 让它们在本文中更好区分。

之后把 “battery_icon” 和 “battery_icon_charge” 这两个Drawable添加到你的模块当中。最简单的情况是添加 “res/drawables/battery_icon.png” 和 “res/drawables/battery_icon_charge.png”。但是你可以使用Android提供的所有方式来定义资源。所以对于动画图标,你可使用带有animation-list和其它Drawable资源引用的XML文件。当然这个XML文件也必须放在你的模块内。

通过这些替换,你能要求Xposed将所有指向特定资源的请求跳到你自己的模块。这同样意味着你可以利用qualifier,比如:如果你对landscape或更低的屏幕密度需要不同的资源。翻译也可以以相同的方式提供。同样,你也许需要这么做如果原来的资源使用qualifier。你不能仅仅替换一段文字的西班牙语版本。正如前面提到的,请求是向前的,所以它会完全被你的模块资源处理,并且不会察觉其它翻译的存在。

这个技巧基本上对所有资源类型都有效。除了极个别的像主题之类的。

# 修改布局

尽管理论上你可以用之前提到的技巧彻底替换布局,但这有很多坏处。你必须从原有布局中复制整个布局,这会降低对其它ROM的兼容性。主题也许回丢失。只有一个模块可以代替布局。如果两个模块都尝试这么做,后者将会胜出。最重要的是,其它资源的ID和引用是很难定义的。因此,我不推荐这样做。

作为一种好的选择,你可以使用后填充的hook。下面是你怎么做:

@Override
public void handleInitPackageResources(InitPackageResourcesParam resparam) throws Throwable {
    if (!resparam.packageName.equals("com.android.systemui"))
        return;

    resparam.res.hookLayout("com.android.systemui", "layout", "status_bar", new XC_LayoutInflated() {
        @Override
        public void handleLayoutInflated(LayoutInflatedParam liparam) throws Throwable {
            TextView clock = (TextView) liparam.view.findViewById(
                    liparam.res.getIdentifier("clock", "id", "com.android.systemui"));
            clock.setTextColor(Color.RED);
        }
    }); 
}

每当“status_bar”布局被填充时,回调方法handleLayoutInflated就被调用。在你以参数形式得到的LayoutInflatedParam对象中,你可以找到刚被创建的View并在需要时修改它。你同样会得到resNames用来鉴别这个方法因哪个布局而被调用(万一你对多个布局使用同一种方法),还有variant,比如可能包含layout-land,如果它就是被加载的布局的版本的话。res帮助你以布局从同一个来源得到ID或额外的资源。

原文地址。主要讲解Xposed框架helper类里的各种方法。

Xposed中有许多Helper方法使得模块的开发变得更为简单。

# XposedBridge类

log

log方法是一个向标准logcat和/data/xposed/debug.log文件输出错误信息的简单方式。它可以捕获日志信息或者一个Throwable对象。后面的例子中,它将打印堆栈踪迹。

hookAllMethods / hookAllConstructors

如果你想把所有的方法都以一个指定的名称hook起来,或是hook类中所有的构造函数,你可以使用这两种方法。当有许多不同的变量,可是你希望在它们当中任何一个被调用之前/后都执行一段代码时,这是非常有用的。时刻记住,其它的ROM也许有额外的变量同样也被hook。尤其小心你在回调时得到的args

# XposedHelpers类

我建议把这个类添加到Eclipse的静态导入的偏好设置中:Window => Preferences => Java => Editor => Content Assist => Favorites => New Type,输入:de.robv.android.xposed.XposedHelpers。这样做的话,eclipse在你开始输入“get…”时会自动提示这个类中的方法,并且会创建这个方法的静态导入(这意味着在你的代码中是看不到类名的)。

findMethod / findConstructor / findField

有许多你不必亲自使用反射获得方法、构造函数和字段的方式。同样,你也可以对几个特定参数种类使用“最佳匹配”来寻找方法和构造函数。比如:你可以用一个TextView类型的参数来调用findMethodBestMatch(Class<?> clazz, String methodName, Object... args)。如果没有更加符合匹配的变量,这同样会找到一个具有你指定的名称而且以View类型对象作为参数的方法。

callMethod / callStaticMethod / newInstance

利用上文提到的findXXX方法,这些方法使得调用方法和创建一个类的实例变得简单。调用者不必为此而使用反射。没有必要事先就取得某个方法,只要及时使用以上的这些方法调用它就可以了。参数类型是从实际的参数值和最匹配的被调用方法中自动拷贝而来。万一你想明确的指出参数的类型,创建一个Class<?>数组,然后把它传递给callXXX/newInstance。你可以使数组中的一些项为空(null)来使用实际参数的类型。但是数组的长度必须和参数的个数匹配。

getXXXField / setXXXField / getStaticXXXField /setStaticXXXField

它们是让你能够轻易获得和设置对象实例和类变量内容的封装器。你只需要对象的引用、字段名、和类型(当然还有提供给setter的新值)。如果你想获得/设置一个静态字段,并且没有对象的引用,你可以使用getStaticXXXsetStaticXXX方法。然而当你有一个对象的引用时,区分静态和实例字段是没有必要的。getXXXsetXXX可以给两者都设定值。

getAdditionalXXXField / setAdditionalXXXField

这些方法让你将任何值与一个对象的实例或是整个类(像是一个静态字段)联系起来。这些值依照关键字-值对的方式存储。所以每个对象可以存储多个值。关键字可以是任何字符串,包括对象实际拥有的字段的名称。请注意你不能通过调用getAdditionalInstanceField获取你之前用setAdditionalStaticField存储的值。取而代之的是应该使用getAdditionalStaticField。它有一个变量来接受对象并自动查询它的类。

assetAsByteArray

这个方法以byte数组的形式返回资源。如果你想加载你模块的资源,你可以使用以下代码:

public class XposedTweakbox implements IXposedHookZygoteInit {
    @Override
    public void initZygote(StartupParam startupParam) throws Throwable {
        Resources tweakboxRes = XModuleResources.createInstance(startupParam.modulePath, null);
        byte[] crtPatch = assetAsByteArray(tweakboxRes, "crtfix_samsung_d506192d5049a4042fb84c0265edfe42.bsdiff");
...

getMD5Sum

返回文件系统中一个文件的MD5值。当前的app需要获取读这个文件的权限(在init方法中你拥有root权限,所以这应该不是什么问题)。

getProcessPid

按照进程的/proc/[pid]/cmdline的第一部分来寻找这个进程,并以字符串形式返回它的PID。

原文地址。这是开发者所写的,可以说是官方开发指南。文章讲述了Xposed的原理,以及怎么开发Xposed框架的模块。头一次翻译技术文档,有错误的话请多包涵。

好了,你想学习怎么为Xposed开发新的模块么?那么读读这篇教程(或者我们可以称他为”泛读短文”)学着怎么去做。这不仅包括“创建这个文件然后插入…”这类的技巧,也包括这些技巧背后的思想。这些思想正是创造价值的步骤以及你真正需要了解你做了什么和为什么这么做的原因。如果你觉得本文“太长,不想读”,那么你可以只看最后的源代码和阅读“使工程成为Xposed模块“部分。但是如果你读了整篇文章你就会有更好的理解。你之后会节省出来阅读这个的时间,因为你不必凭自己弄清楚每件事。

# 修改主题

你将重新创建在github上可以找到的红色钟表的的例子。它包括将状态栏的钟表变为红色并且加入一个笑脸的功能。我选择这个例子是因为它非常小,而且容易看见所做的修改。并且,它也使用了框架所提供的一些基本方法。

# Xposed如何工作

在你开始做出自己的修改之前,你应当大致了解Xposed如何工作(如果觉得这部分无聊可以跳过)。以下就是原理:

有一个叫做”Zygote”的进程,它是android运行环境的核心。每个应用都从一份它的拷贝(“fork”)产生。这个进程在手机启动时由一个叫 /init.rc 的脚本启动。这个进程的启动在 /system/bin/app_process 加载所需要的类和调用初始化方法后完成。

这里就是Xposed发挥用处的地方了。当你安装完框架后,一个扩展过的app_process就会被复制到 /system/bin 下。这个扩展过的启动进程会将一个额外的jar包添加到环境变量,并在特定场合调用里面的方法。比如:当虚拟机创建完成后和Zygote的main方法被调用前。并且在那个方法当中,我们已经是Zygote的一部分,而且能够在它的上下文context中活动。

jar包的位置是 /data/data/de.robv.android.xposed.installer/bin/XposedBridge.jar 它的源代码可以在这里找到。查看XposedBridge的类,你能找到main方法。这就是我上文中所写过的,它在每个进程的最开始部分被调用。一些初始化的工作在那里完成,并且我们的模块在那里加载(之后我再讲模块的加载)。

# 方法的hook/替换

真正使Xpoesed有威力的就是hook方法调用。当你反编译并修改APK时,你能够在任何你想的地方直接修改/替换指令。然而,你事后需要重新编译/给APK签名,并且只能发布整个安装包。使用Xposed能让你放置的hook,你并不能修改程序内部的方法代码(清楚地定义你想要在何处做什么样的修改是不可能的)。然而,你可以在方法调用的前后注入你的代码。这也是java中能够被清楚寻址的最小单位。

XposedBridge 有一个私有的 native 方法叫做 hookMethodNative。这个方法也在扩展后的 app_process 中被实现了。它会将方法类型转为“native”,并把方法的实现与本地的通用方法相连。这意味着,每当被hook的方法调用后,调用者不知道实际调用的是通用的方法。在这个方法中,位于 XposedBridge 的 handleHookedMethod 方法会被调用,并向方法调用传递参数、this指针以及其他东西。之后这个方法负责唤起之前方法调用注册过的回调。上述这些行为能够改变调用的参数、实例/静态变量、唤起其他方法、处理调用结果。。。或者跳过这些东西。它的弹性非常大。

好了,理论讲够了。我们现在创建一个模块吧!

# 创建工程

一个模块就是一个普通的app,只不过多了一些特殊的文件和元数据。所以在我们创建新的android工程以前,我假设你已经做过这个了。如果没有,官方文档讲的很详细。对于SDK,我选择了4.0.3(API15)。我建议你也使用这个,并且不要立刻开始。你不需要创建Activity,因为我们的修改不需要任何用户界面。回答过了这个问题后,你应该有一个空白的工程项目。

# 使工程成为Xposed模块

现在我们把工程变成Xposed能加载的东西。我们需要以下几个步骤。

AndroidManifest.xml

Xposed Installer的模块列表搜寻所有有一种特殊元数据标记的应用程序。你可以到 AndroidManifest.xml => Application => Application Nodes (在底部) => Add => Meta Data 下面去创建这个标记。标记名称应该是 xposedmodule ,值应该是 true。给resource留空。重复以上过程创建 xposedminversion (见下文) 和 xposeddescription (你创建的模块的简单描述)。XML文件现在就是这个样子:

<?xml version="1.0" encoding="utf-8"?>
 <manifest  xmlns:android="http://schemas.android.com/apk/res/android"
    package="de.robv.android.xposed.mods.tutorial"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="15" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <meta-data
            android:name="xposedmodule"
            android:value="true" />
        <meta-data
            android:name="xposeddescription"
            android:value="Easy example which makes the status bar clock red and adds a smiley" />
        <meta-data
            android:name="xposedminversion"
            android:value="30" />
    </application>
</manifest>

XposedBridgeApi.jar

接下来,让程序能够找到 XposedBridge 的API。你可以从 这里下载 XposedBridgeApi-<version>.jar 的最新版。把它复制到叫做lib的子文件夹下。右键单击选择Build Path => Add to Build Path。文件名当中的<version>是你在manifest文件的xposedminversion标签所插入的版本。

保证API类没有被包含(但仅仅是参考)在你编译过的APK里,否则你会得到一个IllegalAccessError错误。libs(含有s)文件夹是eclipse自动生成的,不要把API文件放在那里。

模块的实现

现在你可以给你的模块创建一个类了。我的类叫做”Tutorial”,位于de.robv.android.xposed.mods.tutorial这个包中。

package de.robv.android.xposed.mods.tutorial;

public class Tutorial {

}

第一步,我们仅仅生成一些日志表明模块已经加载。一个模块可以有多个入口点。你选择哪个取决于你想修改什么。你可以在安卓系统启动时、在一个app将要启动时、在一个app的资源文件初始化时或其他时候,调用一个函数。

在这个教程靠后面的一部分,你将了解到在一个特定的app中需要做出的修改。那么先让我们了解一下 “让我知道什么时候加载一个新app” 这个入口点。所有入口点都被标记为IXposedMod的子接口。这种情况下,你需要实现 IXposedHookLoadPackage 这个接口。其实它只有一个仅有一个参数的方法。这个方法向被实现的模块提供更多关于运行环境上下文的信息。在我们的例子中,我们用log输出加载的app的名称。

package de.robv.android.xposed.mods.tutorial;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;

public class Tutorial implements IXposedHookLoadPackage {
    public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
        XposedBridge.log("Loaded app: " + lpparam.packageName);
    }
}

这个log方法向标准logcat以及 /data/data/de.robv.android.xposed.installer/log/debug.log(通过Xposed Installer可以轻易访问到)输出信息(tag Xposed)。

assets/xposed_init

现在唯一遗漏的就是提示XposedBridge哪些类包含了入口点。这项工作通过一个叫 xposed_init 的文件完成。在assets文件夹下创建一个新的名叫xposed_init的text文件。在该文件中每行包含一个类的全名。在这个例子中,它是 de.robv.android.xposed.mods.tutorial.Tutorial。

# 试试看

保存你的文件。以Android Application的方式运行你的程序。因为这是你第一次安装它,在使用前你需要先启用。打开Xposed Installer这个app并确保你安装了xposed框架。之后切换到Modules标签。你应该能在那里找到你的app。在选择框内打钩使得它可用。然后重启。你当然什么变化也看不到,但如果检查log记录,以应该会看见以下的东西:

Loading Xposed (for Zygote)...
Loading modules from   /data/app/de.robv.android.xposed.mods.tutorial-1.apk
Loading class de.robv.android.xposed.mods.tutorial.Tutorial
Loaded app: com.android.systemui
Loaded app: com.android.settings
... (many more apps follow)

瞧!它起作用了。现在你拥有了一个Xposed模块。它能够变得比写一些log更加有用…

# 探索你的目标并寻找修改它的方式

好了,下面要开始讲的部分也许会非常不同,这取决于你想做什么。如果你之前修改过apk,也许你会知道在这里应当如何思考。总的来说,你需要了解目标的一些实现细节。在本教程中,目标选定为状态栏的时钟。这有助于了解到状态栏以及其他一些东西都是系统UI的一部分。现在让我们在那里开始我们的探索。

可能性1:反汇编。这会告诉你它实际的实现,但是会很难阅读和理解,因为你得到的都是smali格式的东西。可能性2:获得AOSP源代码。比如这里这里。ROM不同代码也很不一样,但在本例中他们的实现是相似的甚至是相同的。我会先看AOSP,然后看看这么做够不够。如果我需要细节,我会看看实际的反汇编的代码。

你可以找找名称中有“clock”的类。其他需要找的是用到的资源和布局。如果你下载官方的AOSP代码,你可以从 frameworks/base/packages/SystemUI 开始找。你会找到好几处“clock”出现的地方。找到有好几种方式实现修改是正常而且真实的。时刻记住你“只能” hook方法。所以你必须另找其他能够插入代码实现功能的地方,要么在方法的前面或是后面,或者是替换掉方法。你应当hook一个尽可能明确的方法,而不是一个被调用成千上万次的用于解决性能问题和非计划中的副作用的方法。

在本例当中,你或许会发现布局 res/layout/status_bar.xml 包含一个指向带有类com.android.systemui.statusbar.policy.Clock的自定义view。现在你脑子里也许会有好多点子。文字的颜色是通过textAppearance属性定义的,所以最干净的更改它的方法就是修改外观的定义。然而,用Xposed框架改变外观属性几乎是不可能的(这需要深入本地代码)。替换状态栏的布局也许是可能的,但对于你试图做出的小小修改来说是杀鸡用牛刀。取而代之的是,看看这个类。有一个叫updateLock的方法,似乎每分钟都调用一次用于更新时间。

final void updateClock() {
    mCalendar.setTimeInMillis(System.currentTimeMillis());
    setText(getSmallTime());
}

这个方法用于修改来说是很好的,因为这是一个足够具体的看似唯一能够修改时钟文字的方法。如果我们在这个方法的每次调用之后都加些修改时钟文字和颜色的东西,应该就能起作用。那么,我们开始做吧。

对于单独修改字体颜色部分,有一种更好的办法。参见“替换资源”中“修改布局”的例子。

# 使用反射寻找并hook方法

现在我们已经知道了哪些东西?我们在com.android.systemui.statusbar.policy.Clock有一个叫做updateClock的希望干涉的方法。我们在系统UI资源中找到了这个类,所以它仅仅在系统UI进程当中有效。其它一些类属于框架,而且在任何地方都有效。如果我们在 handleLoadPackage 中试图直接获取任何这个类的信息和引用,就会失败。因为处于错误的进程中。所以让我们实现一种仅在某个特定包即将加载时执行特定代码的情况:

public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable {
    if (!lpparam.packageName.equals("com.android.systemui"))
        return;

    XposedBridge.log("we are in SystemUI!");
}

使用参数,我们能轻松检查是否在正确的包中。一旦我们核实了这一点,我们就通过ClassLoader取得那个包中的也被这个变量引用的类。现在我们可以寻找com.android.systemui.statusbar.policy.Clock这个类以及它的updateClock方法,然后告诉XposedBridge去hook这个方法:

package de.robv.android.xposed.mods.tutorial;

import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;

public class Tutorial implements IXposedHookLoadPackage {
    public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
        if (!lpparam.packageName.equals("com.android.systemui"))
            return;

        findAndHookMethod("com.android.systemui.statusbar.policy.Clock", lpparam.classLoader, "updateClock", new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                // this will be called before the clock was updated by the original method
            }
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                // this will be called after the clock was updated by the original method
            }
    });
    }
}

findAndHookMethod是一个Helper函数。注意静态导入,如果你像连接中说的一样做了,那就会被自动加入。这个方法使用系统UI包的ClassLoader寻找 Clock 这个类,然后寻找其中的updateClock方法。如果这个方法中有任何参数,你必须事后列出这些参数的类型。有许多方法做这件事,但因为我们的方法没有参数,所以就先跳过这个步骤。至于最后一点,你需要提供一个 XC_MethodHook 类的实现。对于更小的更改,你可以使用匿名类。如果你的代码很多,最好创建普通的类并且只在这里创建实例。helper之后会如之前所说的,做出所有hook这个方法的必要工作。

XC_MethodHook中有两个你可以重写的方法。你可以两个都重写,或者都不重写。但后者的做法显然是没有道理的。这些方法就是beforeHookedMethod 和 afterHookedMethod。不难猜出他们在原有方法之前/后执行。你可以使用“before”方法在方法调用前估计/操纵参数(通过param.args)。甚至阻止调用原来的方法(发送你自己的结果)。“after”方法可以用来做一些基于原来方法的结果的事。你也可以在这个地方操纵结果。当然,你可以在方法调用的前/后添加你自己要执行的代码。

如果你要彻底替换一个方法,去看看子类XC_MethodReplacement。在那里,你只需要重写replaceHookedMethod。

XposedBridge为每个hook过的方法维护一个注册过的回调的表。拥有最高优先级(在hookMethod中被定义)的将被优先调用。原有的方法总是具有最低的优先级。所以如果你hook了一个拥有回调A(高优先级)和回调B(默认优先级)的方法,那么不管被hook的方法是如何被调用的,执行顺序总是这样的:A.before -> B.before -> original method -> B.after -> A.after。所以A可以影响B看到的参数,即把它们传递下去以前大幅度地修改它们。原有的方法的结果会先被B处理,但是A拥有原先调用者最终将得到什么样结果的决定权。

最终步骤:在方法调用之前/后执行你自己的代码

好了,你已经在正确的上下文运行环境中(比如:系统UI进程)有了一个在updateClock方法每次被调用时都会被调用的方法。现在让我们修改一些东西吧。

第一个要检查的:我们有具体的Clock类型的引用么?是的,我们有。这就是param.thisObject参数。所以如果方法通过myClock.updateClock()被调用,那么param.thisObject 就是 myClock。

接下来:我们对clock做什么?Clock类型并不可用,你不能将param.thisObject转换为类(也不要试着这样做)。然而它是从TextView继承而来。所以一旦你把Clock的引用转换为TextView,你可以使用像setText, getText 和 setTextColor之类的方法。我们的改动应该在原有的方法设置了新的时间以后进行。因为在方法被调用前什么都没有做,我们可以空着beforeHookedMethod。没有必要调用(空的)“super”方法。

所以以下是完整的源代码:

package de.robv.android.xposed.mods.tutorial;

import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
import android.graphics.Color;
import android.widget.TextView;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;

public class Tutorial implements IXposedHookLoadPackage {
    public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
        if (!lpparam.packageName.equals("com.android.systemui"))
            return;

        findAndHookMethod("com.android.systemui.statusbar.policy.Clock", lpparam.classLoader, "updateClock", new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                TextView tv = (TextView) param.thisObject;
                String text = tv.getText().toString();
                tv.setText(text + " :)");
                tv.setTextColor(Color.RED);
            }
        });
    }
}

# 对结果感到满意

现在启动/安装你的app。因为你第一次启动它时已经在Xposed Installer中把它设置为了可用,你就不需要在做这一步了。重启即可。然而,如果你正在使用这个红色钟表的例子,你也许想禁用它。两者都对它们的updateClock 处理程序使用了默认的优先级。所以你不清楚哪个会胜出(实际上这依赖于处理方法的字符串表示形式,但不要依赖这个方式)。

# 结论

我知道这个教程很长。但我希望你现在不但能实现一个绿色的表,也能够做出一些完全不同的东西。找到好的被hook的方法是经验的问题,所以先从简单的做起。试着一开始多使用log函数确保每次调用都是你预期的。现在:祝玩得开心!

假期和另外几个同学做一个安卓下的安全APP,一起在oschina上建了个私有仓库,用IntelliJ开发。Merge时候有几个文件冲突了。

为了防止提交上去的文件冲突,是一定要在项目的根目录下加上一个名叫”.gitignore”的文件。主要冲突的就是IntelliJ自身的工程配置文件,还有每个人调试程序生成的类以及apk.

主要参考https://github.com/github/gitignore这里的内容写成。

# Built application files
*.apk
*.ap_
# Files for the Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
# Gradle files
.gradle/
build/
/*/build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log

# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
*.iml
## Directory-based project format:
.idea/
# if you remove the above rule, at least ignore the following:
# User-specific stuff:
# .idea/workspace.xml
# .idea/tasks.xml
# .idea/dictionaries
# Sensitive or high-churn files:
# .idea/dataSources.ids
# .idea/dataSources.xml
# .idea/sqlDataSources.xml
# .idea/dynamic.xml
# .idea/uiDesigner.xml
# Gradle:
# .idea/gradle.xml
# .idea/libraries
# Mongo Explorer plugin:
# .idea/mongoSettings.xml
## File-based project format:
*.ipr
*.iws
## Plugin-specific files:
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties

附上gitignore的小教程

比较全面的gitignore用法:http://git-scm.com/docs/gitignore

学分大的课全考完了,今天继续把以前的博客从csdn搬到了这里,然后全部搬完了。看到以前的博客,感觉第一遍学习时很多东西仅仅停留在掌握上,认识还不够深刻。就比如之前写的迷宫生成的程序,这学期末有个老师的班上也布置了这个作业,我看到我们班上有个选这个老师的课的同学,他的算法和我的略有不同(直接挖迷宫,不假设有“小房子”)。用他的算法生成的迷宫只有一条通向终点的路径,用我的方法可以有一条以上的路径。但我暑假写这个程序时却没思考过如何生成只有一个到终点的路径的迷宫。还有动态规划,以前也理解的很浅。现在看看一些以前写的代码,感觉就一字。

这学期自学了一点8086汇编,还写了一个安卓的播放器app,再后来又在写一个linux下的小工具。后来因为一些事情,原先的计划就都停下了。这学期一直没更博客。虽然大二很忙吧,但是也可以采取先挖个坑,以后再填上的办法嘛!

再回头来说对所学东西的认识这个问题。我现在觉得掌握和会用只是学东西的第一步。照着网上或教科书上的教程写个东西,或是用用别人的轮子,这都不算什么。我觉得能把书上东西弄懂,自己还会变通使用才行。用别人的轮子,自己还能修改,甚至别人把你的修改合并进来,那才算厉害。

还有学校开的专业基础课要学好。学好不全是指考试好,而是对学了什么,能用到哪里清清楚楚。实践才是最主要的。考试毕竟和实用还有很大差距。不过一般到了能用的水平,考试也就不会太差了。学校的课程虽然实践性不强,但是所学都是计算机科学的基础,只可惜这些课程大多也只讲授最基本的内容。我觉得对计算机本身构造的理解和算法的设计能力是在学校期间应该非常注意培养。简言之就是:底层+算法。如果对科研有兴趣的话还应该额外看一些数学和算法方面的书。计算机是门实用的学问,任何离开实际去学习的方法都是不可取的。最好的学习方法就是多动手。学习时多把书上看不明白的地方自己实现一遍,自己有好的想法也要实现一遍。以后工作的话估计很忙,能自由支配的学习时间可能不会很多。

另外,自己要培养自己的工程能力。说实话我现在做的还很糟糕,因为目前还没有项目经验(自己写的小玩意儿不算)。不过这不着急,毕竟上学期和这学期还在打基础,而且其实这个学期也做了一些准备工作了。下学期我看看有什么好的机会,要争取参加。

一直想搭建一个真正意义上自己的博客。之前在CSDN上面有博客,但是写的不勤。我觉得博客应该坚持写,把自己学到的知识总结好。其实写文章的过程是回顾自己所学的一个很好的途径。之前在CSDN上想写,但是总是以“ 这学期很忙” 来敷衍自己。我想博客如果是自己搭建的而不仅仅只是注册在某个网上的空间,也许就会认真对待些吧。所以就想到弄个个人的博客站点,顺便再把以前的文章搬运过来。本来说考完期末再弄,但是今天图书馆关门,晚上教学楼太冷,就回寝室自习了。结果在寝室里做题错误率爆涨,只好找点别的事做。大概也就这个事情不太耗费脑力了。正好新年开个头,弄个博客,希望今年自己在技术上多进步些吧。

远程关联操作:

创建自己的仓库后,要关联一个远程库,使用命令git remote add origin git@server-name:path/repo-name.git;

关联后,使用命令git push -u origin master第一次推送master分支的所有内容;

此后,使用命令:git push origin '分支名称' 推送最新修改;

  • 克隆仓库:git clone git@github.com:GTIHUB账户名/仓库名.git
  • git remote -v 查看远程仓库详细信息
  • git branch - -set-upstream 分支名 origin/分支名 指定本地某分支与远程origin/某分支的链接
  • git pull 从服务器同步

本地操作:

  • git status 查看本地仓库状态

  • git add 'filename' 添加文件到本地缓存

  • git add -A 添加所有改动的文件

  • git commit -m 'commitment' 提交更改到本地并添加说明内容

  • 在仓库目录下输入:git init,把这个目录变成仓库

  • git log 显示近三次的更改

  • git log --graph --pretty=oneline --abbrev-commit 查看分支图形

  • git reset --hard HEAD^ 退到上个版本

  • git reset --hard 'SHA-1编码' 退到SHA-1编码表示的版本

  • git diff HEAD -- 'FILE' 查看工作区文件与版本库文件的不同之处(可以省略HEAD后内容)

    git checkout [选项] [<分支>] -- <文件>...

    -q, --quiet           不显示进度报告
    
    -b <分支>             创建并检出一个新的分支
    
    -B <分支>             创建/重置并检出一个分支
    
    -l                    为新的分支创建引用日志
    
    --detach              成为指向该提交的分离头指针
    
    -t, --track           为新的分支设置上游信息
    
    --orphan <新分支>     新的没有父提交的分支
    
    -2, --ours            对尚未合并的文件检出我们的版本
    
    -3, --theirs          对尚未合并的文件检出他们的版本
    
    -f, --force           强制检出(丢弃本地修改)
    
    -m, --merge           和新的分支执行三路合并
    
    --overwrite-ignore    更新忽略的文件(默认)
    
    --conflict <风格>     冲突输出风格(merge 或 diff3)
    
    -p, --patch           交互式挑选数据块
    
    --ignore-skip-worktree-bits 对路径不做稀疏检出的限制

git checkout -- 'readme.txt' 丢弃工作区的更改(后面--参数必须有)

对于提交到了暂存区的更改:先git reset HEAD file

删除文件时先直接把原文件删除。然后git rm '文件',然后commit。如果是误删除git checkout -- '刚才删掉的文件'

在用户个人目录下:ssh-keygen -t rsa -C 'youremail@***.com' 生成密钥

分支管理:

  • 查看分支:git branch
  • 创建分支:git branch name
  • 切换分支:git checkout name
  • 创建+切换分支:git checkout -b name
  • 合并某分支到当前分支:git merge name
  • 删除分支:git branch -d name 换成-D是强行删除
  • git merge --no-ff -m "merge with no-ff" dev 普通的合并模式

BUG 修复时用到的命令:

修复bug时,创建新的bug分支进行修复,然后合并,最后删除; 当手头工作没有完成时,先git stash一下,然后去修复bug,修复后,再git stash pop,回到工作现场。

  • git stash apply stash@{0} 恢复到指定的stash
  • git stash list 查看所有的stash现场
  • git stash pop 恢复工作并删除之前保存的现场
  • git stash 保存工作现场

最近在看回溯搜索算法,觉得值得总结一下。

回溯法简单说就是,为了搜到合适的解做出尝试,尝试失败时就退回到之前状态,然后尝试其他的可能性。这类算法套路比较固定。总的思想我概括为:搜到就输出,搜不到就返回,符合条件就搜。

流程用文字描述,大概分为两类情况:

1.当搜索过程每前进一步,会遇到多种情况时:这类情况的代码形式上类似图的深度遍历

void search(int k){
    if (达到目标) 输出解
    else
       for each(所有可能情况)
       if (满足递归条件){
         保存状态
         search(k+1)
         恢复之前的状态
       }
}

2.当搜索过程每前进一步,只有两种选择时:这类情况的代码形式上类似二叉树的先根遍历

void search(int k){
   if (达到目标) 输出解
   else{
      if(符合选择1条件){
        保存状态和数据
        search(k+1, 参数1)
        恢复
      }
      if(符合选择2条件){
        保存状态和数据
        search(k+1, 参数2)
        恢复
      }
   }        
}

当然不一定所有问题都严格遵守上面的步骤。比如,有的时候搜索的分支可能并没改变数据(之后有举例),这时保存数据这步是可以没有的。

回溯法的优点在于省内存,搜索过程中产生解空间。由于采用了递归和深度优先的策略,最大耗费空间仅仅和搜索的最大深度有关。

第一个例子,给出n和m,从1到n中挑出m个数,产生所有可能的排列数和组合数,n小于10:

#include <iostream>
#include <vector>
using namespace std;

bool used[10]={false};//数字i是否使用过
int num[10];//n,m smaller than 10

int id1=0, id2=0;

vector<int> num2;
bool used2[10]={false};

void p(int n, int r, int k){
    if(k==0){
        id1++;
        cout<<id1<<": ";
        for(int i=r; i>=1; i--)cout<<num[i];
        cout<<"\t";
    }else{
        for(int i=1; i<=n; i++){
            if(!used[i]){
                num[k]=i;
                used[i]=true;
                p(n,r,k-1);
                used[i]=false;
            }
        }
    }
}

void permutation(int n, int r){
    p(n,r,r);
}

void c(int n, int r, int k){
    if((int)num2.size()==r){
        id2++;
        cout<<id2<<": ";
        for(int i=0; i<num2.size(); i++)cout<<num2[i];
        cout<<"\t";
    }else{
        for(int i=k; i<=n; i++){//i=k,保证排列结果是由小到大输出
            if(!used2[i]){
                num2.push_back(i);
                used2[i]=true;//save
                c(n,r,i+1);//search
                used2[i]=false;
                num2.pop_back();//go back

            }
        }
    }
}

void combination(int n, int r){
    c(n,r,1);
}

int main(){
    int n,m;
    cin>>n>>m;
    cout<<"permutation:"<<endl;
    permutation(n,m);
    cout<<endl;
    cout<<"combination"<<endl;
    combination(n,m);
    return 0;
}

当然上面的例子是输出所有的排列和组合。大部分时候我们只是搜索特定的一个结果,或某些结果。因此舍弃不必要的解是很重要的。这就涉及限界,或者说剪枝。如果搜索到某一步时再接着搜肯定没有结果,那么就停止在这个分支的搜索,通常用一个限制条件或外加判断函数来判断。

第二个例子,给定一个集合,知道它有n个元素,希望从n个数中取出若干个使得它们的和为c。第一行输入n和c,第二行输入集合内的数。把可行的数字组合(1组就行)输出,如果没有符合条件的组合,就输出NO SOLUTION。

#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;

int n,c;
int a[9000];
int r=0, mi[9000],s[9000], l[9000];
//s=sum, mi=min, l=answer list
void work(int x, int z){
    if(z==c){
        for(int i=1; i<=r; i++){
            cout<<l[i]<<" ";
            if(i==r) cout<<endl;
        }
        exit(0);
    }else{
    //限制条件,如果当前的和加上剩下所有的数超过目标值,且当前的和加上剩下最小的
    也不超过目标值则继续搜索
        if( x<=n && z+mi[x]<=c && z+s[x]>=c){
            if(z+a[x]<=c){//do1, add a[x], when a[x] can be added
                r++;
                l[r]=a[x];//save

                work(x+1,z+a[x]);//do1

                r--;//back
            }
            work(x+1,z);//do2, not to add a[x]
        }
    }
}
/* 
    x增加时有两个选择,将下一个数挑出,或者不挑出,所以有do1和do2两个选择
*/

int main(){
    scanf("%d%d", &n, &c);
    for(int i=1; i<=n; i++)
        scanf("%d", &a[i]);

    mi[n]=s[n]=a[n];
    for(int i=n-1; i>=1; i--){
        if(a[i]<mi[i+1]) mi[i]=a[i];
        else mi[i]=mi[i+1];
        s[i]=s[i+1]+a[i];
    }
    work(1,0);

    cout<<"No solution!"<<endl;
    return 0;
}

有的时候在递归过程中一些结果可能会被重复搜索(即:走了不同的路线,但是到达了相同目的地 -_-|| ),我们还可以建立一个表储存搜索过的结果,如果发现搜过这个结果了,就不要继续递归,直接从表里读取,这样能避免大量的重复递归。这个实现起来相对容易,就不再举例了。

前两天把CENTOS6.5升级到了CENTOS7。其实就是重新安装了新版。网上提供了在线升级的办法,但是经过本人尝试没有成功,最后下载了ISO镜像安装。

在线升级的方法看这里。但是需要先下载Preupgrade Assistant和redhat-upgrade-tool-cli这两个工具。工具下载地址在这里。一共4个RPM安装包。我在测试在线升级时安装包全部下载好了,只是最后重启后安装时每次到一个地方就自动重启然后重新安装,反复了好多次。

最后刻了个DVD安装好了。如果没有刻录盘可以使用dd命令把iso文件刻到U盘里代替,方法:dd if=xxx.iso of=/dev/sdb (假定U盘是sdb,不过一般都是硬盘sda,u盘sdb,注意sdb后不需要加数字)。如果想恢复U盘,不要在windows下格式化(经过亲自测试容量就成了ISO文件的大小),应该在linux下格式化。命令是:mkfs -t vfat /dev/sdb1。dd指令刻录的时间比较长,大概20多分钟电脑卡住会什么反应也没有。

用刻好的设备(DVD或U盘)引导启动,进入安装程序会出现三行字,第一行:Install CentOS7,第二行:Test this media & install CentOS7,第三行:troubleshooting。如果选第一项直接安装失败(我就是选了第一行后在一个地方黑屏左上角一个光标一直闪,然后就卡住了),就进入Troubleshooting,里边有个install centos7 in basic graphic mode。之后会进入图形界面,记住选中文界面(英语好可以无视)。按照提示安装。

这里我的机子出了些BUG,只要调整/boot分区大小,保存修改结果后就会自动变为3MB。好在安装程序有个自动分区功能。如果出现相同状况或者不知道分区分多大好,可以使用这个功能。

安装好了就会提示重启。不过重启后发现WIN7的启动选项没了T_T。以前CENTOS6.5时好歹还有个OTHER选项。。。。。。

GRUB启动选项的修复:在CENTOS7下不再使用GRUB1,而是改用GRUB2。从此不再有menu.lst文件,而是改成了/boot/grub/grub.cfg这个文件。里面的配置格式和指令也变了。启动选项出现时按c,进入grub的命令行模式,输入ls能查看有哪些硬盘分区。依次输入以下命令:

  • set root=(hd0,msdos1)
  • chainloader +1
  • boot

hd0是硬盘,msdos1是WIN7的C盘。如果没成功就在set root里换几个分区。如果成功了,下次进入CENTOS7,修改/boot/grub/grub.cfg,添加下面内容:

menuentry "WIN7" {
     set root=(hd0,msdos1)
     chainloader +1
}

这些都弄好后,去下载个显卡驱动装上,否则CPU发热超大(我的是AMD RADEON HD 8570M)。下载前注意看是不是最新版,是否支持3.10的linux内核。

最后,CENTOS7废除了service指令,所有服务都改用systemctl指令,如果输入service系统会自动切到systemctl。而且输入setup后进入系统自启动项目的管理页面会发现少了很多系统自启动服务。新的修改方法就是用systemctl指令。

  • systemctl list-unit-files 显示所有服务及状态
  • systemctl start xxx.service 启动服务
  • systemctl stop xxx.service 关闭服务
  • systemctl restart xxx.service 重启服务
  • systemctl enable xxx.service 开机启动服务
  • systemctl disable xxx.service 取消开机启动服务